From 5966e5e9ddca6a1d371f65e07dd4307b4f2f72e6 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Mon, 24 Feb 2025 12:24:32 -0800 Subject: [PATCH 001/197] Modified by www.SourceFiles.app --- gitpython/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 gitpython/.gitkeep diff --git a/gitpython/.gitkeep b/gitpython/.gitkeep new file mode 100644 index 000000000..e69de29bb From fdf2b00c71a1c637e1e11c573d93bff8a551e636 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:49 -0800 Subject: [PATCH 002/197] Modified by www.SourceFiles.app --- CONTRIBUTING.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 8536d7f73..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,15 +0,0 @@ -# How to contribute - -The following is a short step-by-step rundown of what one typically would do to contribute. - -- [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. -- For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`. -- Please try to **write a test that fails unless the contribution is present.** -- Try to avoid massive commits and prefer to take small steps, with one commit for each. -- Feel free to add yourself to AUTHORS file. -- Create a pull request. - -## Fuzzing Test Specific Documentation - -For details related to contributing to the fuzzing test suite and OSS-Fuzz integration, please -refer to the dedicated [fuzzing README](./fuzzing/README.md). From 6b99c61faab302ae6feb29dca91b4be823a706dd Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:49 -0800 Subject: [PATCH 003/197] Modified by www.SourceFiles.app --- gitpython/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 gitpython/.gitkeep diff --git a/gitpython/.gitkeep b/gitpython/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 3d4e19b574bc646bed59eede404b8e9eb7615c90 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:49 -0800 Subject: [PATCH 004/197] Modified by www.SourceFiles.app --- fuzzing/LICENSE-APACHE | 201 ----------------------------------------- 1 file changed, 201 deletions(-) delete mode 100644 fuzzing/LICENSE-APACHE diff --git a/fuzzing/LICENSE-APACHE b/fuzzing/LICENSE-APACHE deleted file mode 100644 index 8dada3eda..000000000 --- a/fuzzing/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. From a620e5f63c23f05fe3b3385925eed13525aa06ed Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:50 -0800 Subject: [PATCH 005/197] Modified by www.SourceFiles.app --- git/__init__.py | 300 ------------------------------------------------ 1 file changed, 300 deletions(-) delete mode 100644 git/__init__.py diff --git a/git/__init__.py b/git/__init__.py deleted file mode 100644 index 1b2360e3a..000000000 --- a/git/__init__.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under the -# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ - -# @PydevCodeAnalysisIgnore - -__all__ = [ - "Actor", - "AmbiguousObjectName", - "BadName", - "BadObject", - "BadObjectType", - "BaseIndexEntry", - "Blob", - "BlobFilter", - "BlockingLockFile", - "CacheError", - "CheckoutError", - "CommandError", - "Commit", - "Diff", - "DiffConstants", - "DiffIndex", - "Diffable", - "FetchInfo", - "Git", - "GitCmdObjectDB", - "GitCommandError", - "GitCommandNotFound", - "GitConfigParser", - "GitDB", - "GitError", - "HEAD", - "Head", - "HookExecutionError", - "INDEX", - "IndexEntry", - "IndexFile", - "IndexObject", - "InvalidDBRoot", - "InvalidGitRepositoryError", - "List", # Deprecated - import this from `typing` instead. - "LockFile", - "NULL_TREE", - "NoSuchPathError", - "ODBError", - "Object", - "Optional", # Deprecated - import this from `typing` instead. - "ParseError", - "PathLike", - "PushInfo", - "RefLog", - "RefLogEntry", - "Reference", - "Remote", - "RemoteProgress", - "RemoteReference", - "Repo", - "RepositoryDirtyError", - "RootModule", - "RootUpdateProgress", - "Sequence", # Deprecated - import from `typing`, or `collections.abc` in 3.9+. - "StageType", - "Stats", - "Submodule", - "SymbolicReference", - "TYPE_CHECKING", # Deprecated - import this from `typing` instead. - "Tag", - "TagObject", - "TagReference", - "Tree", - "TreeModifier", - "Tuple", # Deprecated - import this from `typing` instead. - "Union", # Deprecated - import this from `typing` instead. - "UnmergedEntriesError", - "UnsafeOptionError", - "UnsafeProtocolError", - "UnsupportedOperation", - "UpdateProgress", - "WorkTreeRepositoryUnsupported", - "refresh", - "remove_password_if_present", - "rmtree", - "safe_decode", - "to_hex_sha", -] - -__version__ = "git" - -from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union - -if TYPE_CHECKING: - from types import ModuleType - -import warnings - -from gitdb.util import to_hex_sha - -from git.exc import ( - AmbiguousObjectName, - BadName, - BadObject, - BadObjectType, - CacheError, - CheckoutError, - CommandError, - GitCommandError, - GitCommandNotFound, - GitError, - HookExecutionError, - InvalidDBRoot, - InvalidGitRepositoryError, - NoSuchPathError, - ODBError, - ParseError, - RepositoryDirtyError, - UnmergedEntriesError, - UnsafeOptionError, - UnsafeProtocolError, - UnsupportedOperation, - WorkTreeRepositoryUnsupported, -) -from git.types import PathLike - -try: - from git.compat import safe_decode # @NoMove - from git.config import GitConfigParser # @NoMove - from git.objects import ( # @NoMove - Blob, - Commit, - IndexObject, - Object, - RootModule, - RootUpdateProgress, - Submodule, - TagObject, - Tree, - TreeModifier, - UpdateProgress, - ) - from git.refs import ( # @NoMove - HEAD, - Head, - RefLog, - RefLogEntry, - Reference, - RemoteReference, - SymbolicReference, - Tag, - TagReference, - ) - from git.diff import ( # @NoMove - INDEX, - NULL_TREE, - Diff, - DiffConstants, - DiffIndex, - Diffable, - ) - from git.db import GitCmdObjectDB, GitDB # @NoMove - from git.cmd import Git # @NoMove - from git.repo import Repo # @NoMove - from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove - from git.index import ( # @NoMove - BaseIndexEntry, - BlobFilter, - CheckoutError, - IndexEntry, - IndexFile, - StageType, - # NOTE: This tells type checkers what util resolves to. We delete it, and it is - # really resolved by __getattr__, which warns. See below on what to use instead. - util, - ) - from git.util import ( # @NoMove - Actor, - BlockingLockFile, - LockFile, - Stats, - remove_password_if_present, - rmtree, - ) -except GitError as _exc: - raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc - - -def _warned_import(message: str, fullname: str) -> "ModuleType": - import importlib - - warnings.warn(message, DeprecationWarning, stacklevel=3) - return importlib.import_module(fullname) - - -def _getattr(name: str) -> Any: - # TODO: If __version__ is made dynamic and lazily fetched, put that case right here. - - if name == "util": - return _warned_import( - "The expression `git.util` and the import `from git import util` actually " - "reference git.index.util, and not the git.util module accessed in " - '`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially ' - "confusing behavior is currently preserved for compatibility, but may be " - "changed in the future and should not be relied on.", - fullname="git.index.util", - ) - - for names, prefix in ( - ({"head", "log", "reference", "symbolic", "tag"}, "git.refs"), - ({"base", "fun", "typ"}, "git.index"), - ): - if name not in names: - continue - - fullname = f"{prefix}.{name}" - - return _warned_import( - f"{__name__}.{name} is a private alias of {fullname} and subject to " - f"immediate removal. Use {fullname} instead.", - fullname=fullname, - ) - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -if not TYPE_CHECKING: - # NOTE: The expression `git.util` gives git.index.util and `from git import util` - # imports git.index.util, NOT git.util. It may not be feasible to change this until - # the next major version, to avoid breaking code inadvertently relying on it. - # - # - If git.index.util *is* what you want, use (or import from) that, to avoid - # confusion. - # - # - To use the "real" git.util module, write `from git.util import ...`, or if - # necessary access it as `sys.modules["git.util"]`. - # - # Note also that `import git.util` technically imports the "real" git.util... but - # the *expression* `git.util` after doing so is still git.index.util! - # - # (This situation differs from that of other indirect-submodule imports that are - # unambiguously non-public and subject to immediate removal. Here, the public - # git.util module, though different, makes less discoverable that the expression - # `git.util` refers to a non-public attribute of the git module.) - # - # This had originally come about by a wildcard import. Now that all intended imports - # are explicit, the intuitive but potentially incompatible binding occurs due to the - # usual rules for Python submodule bindings. So for now we replace that binding with - # git.index.util, delete that, and let __getattr__ handle it and issue a warning. - # - # For the same runtime behavior, it would be enough to forgo importing util, and - # delete util as created naturally; __getattr__ would behave the same. But type - # checkers would not know what util refers to when accessed as an attribute of git. - del util - - # This is "hidden" to preserve static checking for undefined/misspelled attributes. - __getattr__ = _getattr - -# { Initialize git executable path - -GIT_OK = None - - -def refresh(path: Optional[PathLike] = None) -> None: - """Convenience method for setting the git executable path. - - :param path: - Optional path to the Git executable. If not absolute, it is resolved - immediately, relative to the current directory. - - :note: - The `path` parameter is usually omitted and cannot be used to specify a custom - command whose location is looked up in a path search on each call. See - :meth:`Git.refresh ` for details on how to achieve this. - - :note: - This calls :meth:`Git.refresh ` and sets other global - configuration according to the effect of doing so. As such, this function should - usually be used instead of using :meth:`Git.refresh ` or - :meth:`FetchInfo.refresh ` directly. - - :note: - This function is called automatically, with no arguments, at import time. - """ - global GIT_OK - GIT_OK = False - - if not Git.refresh(path=path): - return - if not FetchInfo.refresh(): # noqa: F405 - return # type: ignore[unreachable] - - GIT_OK = True - - -try: - refresh() -except Exception as _exc: - raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc - -# } END initialize git executable path From e20a363eaaffd3df57373b2248ada1c243092dac Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:50 -0800 Subject: [PATCH 006/197] Modified by www.SourceFiles.app --- git/cmd.py | 1724 ---------------------------------------------------- 1 file changed, 1724 deletions(-) delete mode 100644 git/cmd.py diff --git a/git/cmd.py b/git/cmd.py deleted file mode 100644 index 2048a43fa..000000000 --- a/git/cmd.py +++ /dev/null @@ -1,1724 +0,0 @@ -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under the -# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ - -from __future__ import annotations - -__all__ = ["GitMeta", "Git"] - -import contextlib -import io -import itertools -import logging -import os -import re -import signal -import subprocess -from subprocess import DEVNULL, PIPE, Popen -import sys -from textwrap import dedent -import threading -import warnings - -from git.compat import defenc, force_bytes, safe_decode -from git.exc import ( - CommandError, - GitCommandError, - GitCommandNotFound, - UnsafeOptionError, - UnsafeProtocolError, -) -from git.util import ( - cygpath, - expand_path, - is_cygwin_git, - patch_env, - remove_password_if_present, - stream_copy, -) - -# typing --------------------------------------------------------------------------- - -from typing import ( - Any, - AnyStr, - BinaryIO, - Callable, - Dict, - IO, - Iterator, - List, - Mapping, - Optional, - Sequence, - TYPE_CHECKING, - TextIO, - Tuple, - Union, - cast, - overload, -) - -from git.types import Literal, PathLike, TBD - -if TYPE_CHECKING: - from git.diff import DiffIndex - from git.repo.base import Repo - -# --------------------------------------------------------------------------------- - -execute_kwargs = { - "istream", - "with_extended_output", - "with_exceptions", - "as_process", - "output_stream", - "stdout_as_string", - "kill_after_timeout", - "with_stdout", - "universal_newlines", - "shell", - "env", - "max_chunk_size", - "strip_newline_in_stdout", -} - -_logger = logging.getLogger(__name__) - - -# ============================================================================== -## @name Utilities -# ------------------------------------------------------------------------------ -# Documentation -## @{ - - -def handle_process_output( - process: "Git.AutoInterrupt" | Popen, - stdout_handler: Union[ - None, - Callable[[AnyStr], None], - Callable[[List[AnyStr]], None], - Callable[[bytes, "Repo", "DiffIndex"], None], - ], - stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]], - finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None, - decode_streams: bool = True, - kill_after_timeout: Union[None, float] = None, -) -> None: - R"""Register for notifications to learn that process output is ready to read, and - dispatch lines to the respective line handlers. - - This function returns once the finalizer returns. - - :param process: - :class:`subprocess.Popen` instance. - - :param stdout_handler: - f(stdout_line_string), or ``None``. - - :param stderr_handler: - f(stderr_line_string), or ``None``. - - :param finalizer: - f(proc) - wait for proc to finish. - - :param decode_streams: - Assume stdout/stderr streams are binary and decode them before pushing their - contents to handlers. - - This defaults to ``True``. Set it to ``False`` if: - - - ``universal_newlines == True``, as then streams are in text mode, or - - decoding must happen later, such as for :class:`~git.diff.Diff`\s. - - :param kill_after_timeout: - :class:`float` or ``None``, Default = ``None`` - - To specify a timeout in seconds for the git command, after which the process - should be killed. - """ - - # Use 2 "pump" threads and wait for both to finish. - def pump_stream( - cmdline: List[str], - name: str, - stream: Union[BinaryIO, TextIO], - is_decode: bool, - handler: Union[None, Callable[[Union[bytes, str]], None]], - ) -> None: - try: - for line in stream: - if handler: - if is_decode: - assert isinstance(line, bytes) - line_str = line.decode(defenc) - handler(line_str) - else: - handler(line) - - except Exception as ex: - _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") - if "I/O operation on closed file" not in str(ex): - # Only reraise if the error was not due to the stream closing. - raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex - finally: - stream.close() - - if hasattr(process, "proc"): - process = cast("Git.AutoInterrupt", process) - cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "") - p_stdout = process.proc.stdout if process.proc else None - p_stderr = process.proc.stderr if process.proc else None - else: - process = cast(Popen, process) # type: ignore[redundant-cast] - cmdline = getattr(process, "args", "") - p_stdout = process.stdout - p_stderr = process.stderr - - if not isinstance(cmdline, (tuple, list)): - cmdline = cmdline.split() - - pumps: List[Tuple[str, IO, Callable[..., None] | None]] = [] - if p_stdout: - pumps.append(("stdout", p_stdout, stdout_handler)) - if p_stderr: - pumps.append(("stderr", p_stderr, stderr_handler)) - - threads: List[threading.Thread] = [] - - for name, stream, handler in pumps: - t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler)) - t.daemon = True - t.start() - threads.append(t) - - # FIXME: Why join? Will block if stdin needs feeding... - for t in threads: - t.join(timeout=kill_after_timeout) - if t.is_alive(): - if isinstance(process, Git.AutoInterrupt): - process._terminate() - else: # Don't want to deal with the other case. - raise RuntimeError( - "Thread join() timed out in cmd.handle_process_output()." - f" kill_after_timeout={kill_after_timeout} seconds" - ) - if stderr_handler: - error_str: Union[str, bytes] = ( - "error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds" - ) - if not decode_streams and isinstance(p_stderr, BinaryIO): - # Assume stderr_handler needs binary input. - error_str = cast(str, error_str) - error_str = error_str.encode() - # We ignore typing on the next line because mypy does not like the way - # we inferred that stderr takes str or bytes. - stderr_handler(error_str) # type: ignore[arg-type] - - if finalizer: - finalizer(process) - - -safer_popen: Callable[..., Popen] - -if sys.platform == "win32": - - def _safer_popen_windows( - command: Union[str, Sequence[Any]], - *, - shell: bool = False, - env: Optional[Mapping[str, str]] = None, - **kwargs: Any, - ) -> Popen: - """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the - search. - - This avoids an untrusted search path condition where a file like ``git.exe`` in - a malicious repository would be run when GitPython operates on the repository. - The process using GitPython may have an untrusted repository's working tree as - its current working directory. Some operations may temporarily change to that - directory before running a subprocess. In addition, while by default GitPython - does not run external commands with a shell, it can be made to do so, in which - case the CWD of the subprocess, which GitPython usually sets to a repository - working tree, can itself be searched automatically by the shell. This wrapper - covers all those cases. - - :note: - This currently works by setting the - :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during - subprocess creation. It also takes care of passing Windows-specific process - creation flags, but that is unrelated to path search. - - :note: - The current implementation contains a race condition on :attr:`os.environ`. - GitPython isn't thread-safe, but a program using it on one thread should - ideally be able to mutate :attr:`os.environ` on another, without - unpredictable results. See comments in: - https://github.com/gitpython-developers/GitPython/pull/1650 - """ - # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. - # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal - # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP - creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP - - # When using a shell, the shell is the direct subprocess, so the variable must - # be set in its environment, to affect its search behavior. - if shell: - # The original may be immutable, or the caller may reuse it. Mutate a copy. - env = {} if env is None else dict(env) - env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be an value. - - # When not using a shell, the current process does the search in a - # CreateProcessW API call, so the variable must be set in our environment. With - # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283 - # is patched. In Python versions where it is unpatched, and in the rare case the - # ComSpec environment variable is unset, the search for the shell itself is - # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here, - # is simpler and protects against that. (As above, the "1" can be any value.) - with patch_env("NoDefaultCurrentDirectoryInExePath", "1"): - return Popen( - command, - shell=shell, - env=env, - creationflags=creationflags, - **kwargs, - ) - - safer_popen = _safer_popen_windows -else: - safer_popen = Popen - - -def dashify(string: str) -> str: - return string.replace("_", "-") - - -def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]: - return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} - - -def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None: - for k, v in d.items(): - setattr(self, k, v) - for k in excluded: - setattr(self, k, None) - - -## -- End Utilities -- @} - -_USE_SHELL_DEFAULT_MESSAGE = ( - "Git.USE_SHELL is deprecated, because only its default value of False is safe. " - "It will be removed in a future release." -) - -_USE_SHELL_DANGER_MESSAGE = ( - "Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special " - "shell syntax cannot usually be accounted for. This can result in a command " - "injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated " - "and will be removed in a future release." -) - - -def _warn_use_shell(extra_danger: bool) -> None: - warnings.warn( - _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE, - DeprecationWarning, - stacklevel=3, - ) - - -class _GitMeta(type): - """Metaclass for :class:`Git`. - - This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used. - """ - - def __getattribute(cls, name: str) -> Any: - if name == "USE_SHELL": - _warn_use_shell(False) - return super().__getattribute__(name) - - def __setattr(cls, name: str, value: Any) -> Any: - if name == "USE_SHELL": - _warn_use_shell(value) - super().__setattr__(name, value) - - if not TYPE_CHECKING: - # To preserve static checking for undefined/misspelled attributes while letting - # the methods' bodies be type-checked, these are defined as non-special methods, - # then bound to special names out of view of static type checkers. (The original - # names invoke name mangling (leading "__") to avoid confusion in other scopes.) - __getattribute__ = __getattribute - __setattr__ = __setattr - - -GitMeta = _GitMeta -"""Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass. - -Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a -custom metaclass is not documented and may change at any time. This statically checkable -metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be -used. Code that benefits from it is likely to be remain brittle even if it is used. - -In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic -callable attributes representing git subcommands, it rarely makes sense to inherit from -:class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky -to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such -as when a sibling class has an unrelated metaclass and a shared lower bound metaclass -might have to be introduced to solve a metaclass conflict, is not recommended. - -:note: - The correct static type of the :class:`Git` class itself, and any subclasses, is - ``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.) - - :class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is - intended or otherwise possible to use. This alias is truly only for very rare and - inherently precarious situations where it is necessary to deal with the metaclass - explicitly. -""" - - -class Git(metaclass=_GitMeta): - """The Git class manages communication with the Git binary. - - It provides a convenient interface to calling the Git binary, such as in:: - - g = Git( git_dir ) - g.init() # calls 'git init' program - rval = g.ls_files() # calls 'git ls-files' program - - Debugging: - - * Set the :envvar:`GIT_PYTHON_TRACE` environment variable to print each invocation - of the command to stdout. - * Set its value to ``full`` to see details about the returned values. - """ - - __slots__ = ( - "_working_dir", - "cat_file_all", - "cat_file_header", - "_version_info", - "_version_info_token", - "_git_options", - "_persistent_git_options", - "_environment", - ) - - _excluded_ = ( - "cat_file_all", - "cat_file_header", - "_version_info", - "_version_info_token", - ) - - re_unsafe_protocol = re.compile(r"(.+)::.+") - - def __getstate__(self) -> Dict[str, Any]: - return slots_to_dict(self, exclude=self._excluded_) - - def __setstate__(self, d: Dict[str, Any]) -> None: - dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_) - - # CONFIGURATION - - git_exec_name = "git" - """Default git command that should work on Linux, Windows, and other systems.""" - - GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) - """Enables debugging of GitPython's git commands.""" - - USE_SHELL: bool = False - """Deprecated. If set to ``True``, a shell will be used when executing git commands. - - Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython - functions should be updated to use the default value of ``False`` instead. ``True`` - is unsafe unless the effect of syntax treated specially by the shell is fully - considered and accounted for, which is not possible under most circumstances. As - detailed below, it is also no longer needed, even where it had been in the past. - - It is in many if not most cases a command injection vulnerability for an application - to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted - fragment of text to make its way into any part of any argument to any git command - (including paths, branch names, etc.) can cause the shell to read and write - arbitrary files and execute arbitrary commands. Innocent input may also accidentally - contain special shell syntax, leading to inadvertent malfunctions. - - In addition, how a value of ``True`` interacts with some aspects of GitPython's - operation is not precisely specified and may change without warning, even before - GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes: - - * Whether or how GitPython automatically customizes the shell environment. - - * Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of - separate arguments even when ``shell=True``), this can be used with any GitPython - functionality other than direct calls to the :meth:`execute` method. - - * Whether any GitPython feature that runs git commands ever attempts to partially - sanitize data a shell may treat specially. Currently this is not done. - - Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows - in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as - GitPython solves that problem more robustly and safely by using the - ``CREATE_NO_WINDOW`` process creation flag on Windows. - - Because Windows path search differs subtly based on whether a shell is used, in rare - cases changing this from ``True`` to ``False`` may keep an unusual git "executable", - such as a batch file, from being found. To fix this, set the command name or full - path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the - full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim). - - Further reading: - - * :meth:`Git.execute` (on the ``shell`` parameter). - * https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a - * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags - * https://github.com/python/cpython/issues/91558#issuecomment-1100942950 - * https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw - """ - - _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" - _refresh_env_var = "GIT_PYTHON_REFRESH" - - GIT_PYTHON_GIT_EXECUTABLE = None - """Provide the full path to the git executable. Otherwise it assumes git is in the - executable search path. - - :note: - The git executable is actually found during the refresh step in the top level - ``__init__``. It can also be changed by explicitly calling :func:`git.refresh`. - """ - - _refresh_token = object() # Since None would match an initial _version_info_token. - - @classmethod - def refresh(cls, path: Union[None, PathLike] = None) -> bool: - """Update information about the git executable :class:`Git` objects will use. - - Called by the :func:`git.refresh` function in the top level ``__init__``. - - :param path: - Optional path to the git executable. If not absolute, it is resolved - immediately, relative to the current directory. (See note below.) - - :note: - The top-level :func:`git.refresh` should be preferred because it calls this - method and may also update other state accordingly. - - :note: - There are three different ways to specify the command that refreshing causes - to be used for git: - - 1. Pass no `path` argument and do not set the - :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable. The command - name ``git`` is used. It is looked up in a path search by the system, in - each command run (roughly similar to how git is found when running - ``git`` commands manually). This is usually the desired behavior. - - 2. Pass no `path` argument but set the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` - environment variable. The command given as the value of that variable is - used. This may be a simple command or an arbitrary path. It is looked up - in each command run. Setting :envvar:`GIT_PYTHON_GIT_EXECUTABLE` to - ``git`` has the same effect as not setting it. - - 3. Pass a `path` argument. This path, if not absolute, is immediately - resolved, relative to the current directory. This resolution occurs at - the time of the refresh. When git commands are run, they are run using - that previously resolved path. If a `path` argument is passed, the - :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable is not - consulted. - - :note: - Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class - attribute, which can be read on the :class:`Git` class or any of its - instances to check what command is used to run git. This attribute should - not be confused with the related :envvar:`GIT_PYTHON_GIT_EXECUTABLE` - environment variable. The class attribute is set no matter how refreshing is - performed. - """ - # Discern which path to refresh with. - if path is not None: - new_git = os.path.expanduser(path) - new_git = os.path.abspath(new_git) - else: - new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name) - - # Keep track of the old and new git executable path. - old_git = cls.GIT_PYTHON_GIT_EXECUTABLE - old_refresh_token = cls._refresh_token - cls.GIT_PYTHON_GIT_EXECUTABLE = new_git - cls._refresh_token = object() - - # Test if the new git executable path is valid. A GitCommandNotFound error is - # raised by us. A PermissionError is raised if the git executable cannot be - # executed for whatever reason. - has_git = False - try: - cls().version() - has_git = True - except (GitCommandNotFound, PermissionError): - pass - - # Warn or raise exception if test failed. - if not has_git: - err = ( - dedent( - """\ - Bad git executable. - The git executable must be specified in one of the following ways: - - be included in your $PATH - - be set via $%s - - explicitly set via git.refresh() - """ - ) - % cls._git_exec_env_var - ) - - # Revert to whatever the old_git was. - cls.GIT_PYTHON_GIT_EXECUTABLE = old_git - cls._refresh_token = old_refresh_token - - if old_git is None: - # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only - # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value. - - # Determine what the user wants to happen during the initial refresh. We - # expect GIT_PYTHON_REFRESH to either be unset or be one of the - # following values: - # - # 0|q|quiet|s|silence|silent|n|none - # 1|w|warn|warning|l|log - # 2|r|raise|e|error|exception - - mode = os.environ.get(cls._refresh_env_var, "raise").lower() - - quiet = ["quiet", "q", "silence", "s", "silent", "none", "n", "0"] - warn = ["warn", "w", "warning", "log", "l", "1"] - error = ["error", "e", "exception", "raise", "r", "2"] - - if mode in quiet: - pass - elif mode in warn or mode in error: - err = dedent( - """\ - %s - All git commands will error until this is rectified. - - This initial message can be silenced or aggravated in the future by setting the - $%s environment variable. Use one of the following values: - - %s: for no message or exception - - %s: for a warning message (logging level CRITICAL, displayed by default) - - %s: for a raised exception - - Example: - export %s=%s - """ - ) % ( - err, - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - cls._refresh_env_var, - quiet[0], - ) - - if mode in warn: - _logger.critical(err) - else: - raise ImportError(err) - else: - err = dedent( - """\ - %s environment variable has been set but it has been set with an invalid value. - - Use only the following values: - - %s: for no message or exception - - %s: for a warning message (logging level CRITICAL, displayed by default) - - %s: for a raised exception - """ - ) % ( - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - ) - raise ImportError(err) - - # We get here if this was the initial refresh and the refresh mode was - # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we - # discern the difference between the first refresh at import time - # and subsequent calls to git.refresh or this refresh method. - cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name - else: - # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer - # None) we raise an exception. - raise GitCommandNotFound(new_git, err) - - return has_git - - @classmethod - def is_cygwin(cls) -> bool: - return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE) - - @overload - @classmethod - def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ... - - @overload - @classmethod - def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ... - - @classmethod - def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: - """Remove any backslashes from URLs to be written in config files. - - Windows might create config files containing paths with backslashes, but git - stops liking them as it will escape the backslashes. Hence we undo the escaping - just to be sure. - """ - if is_cygwin is None: - is_cygwin = cls.is_cygwin() - - if is_cygwin: - url = cygpath(url) - else: - url = os.path.expandvars(url) - if url.startswith("~"): - url = os.path.expanduser(url) - url = url.replace("\\\\", "\\").replace("\\", "/") - return url - - @classmethod - def check_unsafe_protocols(cls, url: str) -> None: - """Check for unsafe protocols. - - Apart from the usual protocols (http, git, ssh), Git allows "remote helpers" - that have the form ``::
``. One of these helpers (``ext::``) - can be used to invoke any arbitrary command. - - See: - - - https://git-scm.com/docs/gitremote-helpers - - https://git-scm.com/docs/git-remote-ext - """ - match = cls.re_unsafe_protocol.match(url) - if match: - protocol = match.group(1) - raise UnsafeProtocolError( - f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." - ) - - @classmethod - def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None: - """Check for unsafe options. - - Some options that are passed to ``git `` can be used to execute - arbitrary commands. These are blocked by default. - """ - # Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to - # check if they start with "--foo" or if they are equal to "foo". - bare_unsafe_options = [option.lstrip("-") for option in unsafe_options] - for option in options: - for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options): - if option.startswith(unsafe_option) or option == bare_option: - raise UnsafeOptionError( - f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it." - ) - - class AutoInterrupt: - """Process wrapper that terminates the wrapped process on finalization. - - This kills/interrupts the stored process instance once this instance goes out of - scope. It is used to prevent processes piling up in case iterators stop reading. - - All attributes are wired through to the contained process object. - - The wait method is overridden to perform automatic status code checking and - possibly raise. - """ - - __slots__ = ("proc", "args", "status") - - # If this is non-zero it will override any status code during _terminate, used - # to prevent race conditions in testing. - _status_code_if_terminate: int = 0 - - def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: - self.proc = proc - self.args = args - self.status: Union[int, None] = None - - def _terminate(self) -> None: - """Terminate the underlying process.""" - if self.proc is None: - return - - proc = self.proc - self.proc = None - if proc.stdin: - proc.stdin.close() - if proc.stdout: - proc.stdout.close() - if proc.stderr: - proc.stderr.close() - # Did the process finish already so we have a return code? - try: - if proc.poll() is not None: - self.status = self._status_code_if_terminate or proc.poll() - return - except OSError as ex: - _logger.info("Ignored error after process had died: %r", ex) - - # It can be that nothing really exists anymore... - if os is None or getattr(os, "kill", None) is None: - return - - # Try to kill it. - try: - proc.terminate() - status = proc.wait() # Ensure the process goes away. - - self.status = self._status_code_if_terminate or status - except OSError as ex: - _logger.info("Ignored error after process had died: %r", ex) - # END exception handling - - def __del__(self) -> None: - self._terminate() - - def __getattr__(self, attr: str) -> Any: - return getattr(self.proc, attr) - - # TODO: Bad choice to mimic `proc.wait()` but with different args. - def wait(self, stderr: Union[None, str, bytes] = b"") -> int: - """Wait for the process and return its status code. - - :param stderr: - Previously read value of stderr, in case stderr is already closed. - - :warn: - May deadlock if output or error pipes are used and not handled - separately. - - :raise git.exc.GitCommandError: - If the return status is not 0. - """ - if stderr is None: - stderr_b = b"" - stderr_b = force_bytes(data=stderr, encoding="utf-8") - status: Union[int, None] - if self.proc is not None: - status = self.proc.wait() - p_stderr = self.proc.stderr - else: # Assume the underlying proc was killed earlier or never existed. - status = self.status - p_stderr = None - - def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: - if stream: - try: - return stderr_b + force_bytes(stream.read()) - except (OSError, ValueError): - return stderr_b or b"" - else: - return stderr_b or b"" - - # END status handling - - if status != 0: - errstr = read_all_from_possibly_closed_stream(p_stderr) - _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,)) - raise GitCommandError(remove_password_if_present(self.args), status, errstr) - return status - - # END auto interrupt - - class CatFileContentStream: - """Object representing a sized read-only stream returning the contents of - an object. - - This behaves like a stream, but counts the data read and simulates an empty - stream once our sized content region is empty. - - If not all data are read to the end of the object's lifetime, we read the - rest to ensure the underlying stream continues to work. - """ - - __slots__ = ("_stream", "_nbr", "_size") - - def __init__(self, size: int, stream: IO[bytes]) -> None: - self._stream = stream - self._size = size - self._nbr = 0 # Number of bytes read. - - # Special case: If the object is empty, has null bytes, get the final - # newline right away. - if size == 0: - stream.read(1) - # END handle empty streams - - def read(self, size: int = -1) -> bytes: - bytes_left = self._size - self._nbr - if bytes_left == 0: - return b"" - if size > -1: - # Ensure we don't try to read past our limit. - size = min(bytes_left, size) - else: - # They try to read all, make sure it's not more than what remains. - size = bytes_left - # END check early depletion - data = self._stream.read(size) - self._nbr += len(data) - - # Check for depletion, read our final byte to make the stream usable by - # others. - if self._size - self._nbr == 0: - self._stream.read(1) # final newline - # END finish reading - return data - - def readline(self, size: int = -1) -> bytes: - if self._nbr == self._size: - return b"" - - # Clamp size to lowest allowed value. - bytes_left = self._size - self._nbr - if size > -1: - size = min(bytes_left, size) - else: - size = bytes_left - # END handle size - - data = self._stream.readline(size) - self._nbr += len(data) - - # Handle final byte. - if self._size - self._nbr == 0: - self._stream.read(1) - # END finish reading - - return data - - def readlines(self, size: int = -1) -> List[bytes]: - if self._nbr == self._size: - return [] - - # Leave all additional logic to our readline method, we just check the size. - out = [] - nbr = 0 - while True: - line = self.readline() - if not line: - break - out.append(line) - if size > -1: - nbr += len(line) - if nbr > size: - break - # END handle size constraint - # END readline loop - return out - - # skipcq: PYL-E0301 - def __iter__(self) -> "Git.CatFileContentStream": - return self - - def __next__(self) -> bytes: - line = self.readline() - if not line: - raise StopIteration - - return line - - next = __next__ - - def __del__(self) -> None: - bytes_left = self._size - self._nbr - if bytes_left: - # Read and discard - seeking is impossible within a stream. - # This includes any terminating newline. - self._stream.read(bytes_left + 1) - # END handle incomplete read - - def __init__(self, working_dir: Union[None, PathLike] = None) -> None: - """Initialize this instance with: - - :param working_dir: - Git directory we should work in. If ``None``, we always work in the current - directory as returned by :func:`os.getcwd`. - This is meant to be the working tree directory if available, or the - ``.git`` directory in case of bare repositories. - """ - super().__init__() - self._working_dir = expand_path(working_dir) - self._git_options: Union[List[str], Tuple[str, ...]] = () - self._persistent_git_options: List[str] = [] - - # Extra environment variables to pass to git commands - self._environment: Dict[str, str] = {} - - # Cached version slots - self._version_info: Union[Tuple[int, ...], None] = None - self._version_info_token: object = None - - # Cached command slots - self.cat_file_header: Union[None, TBD] = None - self.cat_file_all: Union[None, TBD] = None - - def __getattribute__(self, name: str) -> Any: - if name == "USE_SHELL": - _warn_use_shell(False) - return super().__getattribute__(name) - - def __getattr__(self, name: str) -> Any: - """A convenience method as it allows to call the command as if it was an object. - - :return: - Callable object that will execute call :meth:`_call_process` with your - arguments. - """ - if name.startswith("_"): - return super().__getattribute__(name) - return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - - def set_persistent_git_options(self, **kwargs: Any) -> None: - """Specify command line options to the git executable for subsequent - subcommand calls. - - :param kwargs: - A dict of keyword arguments. - These arguments are passed as in :meth:`_call_process`, but will be passed - to the git command rather than the subcommand. - """ - - self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) - - @property - def working_dir(self) -> Union[None, PathLike]: - """:return: Git directory we are working on""" - return self._working_dir - - @property - def version_info(self) -> Tuple[int, ...]: - """ - :return: Tuple with integers representing the major, minor and additional - version numbers as parsed from :manpage:`git-version(1)`. Up to four fields - are used. - - This value is generated on demand and is cached. - """ - # Refreshing is global, but version_info caching is per-instance. - refresh_token = self._refresh_token # Copy token in case of concurrent refresh. - - # Use the cached version if obtained after the most recent refresh. - if self._version_info_token is refresh_token: - assert self._version_info is not None, "Bug: corrupted token-check state" - return self._version_info - - # Run "git version" and parse it. - process_version = self._call_process("version") - version_string = process_version.split(" ")[2] - version_fields = version_string.split(".")[:4] - leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields) - self._version_info = tuple(map(int, leading_numeric_fields)) - - # This value will be considered valid until the next refresh. - self._version_info_token = refresh_token - return self._version_info - - @overload - def execute( - self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[True], - ) -> "AutoInterrupt": ... - - @overload - def execute( - self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[True], - ) -> Union[str, Tuple[int, str, str]]: ... - - @overload - def execute( - self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[False] = False, - ) -> Union[bytes, Tuple[int, bytes, str]]: ... - - @overload - def execute( - self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[True], - ) -> str: ... - - @overload - def execute( - self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[False], - ) -> bytes: ... - - def execute( - self, - command: Union[str, Sequence[Any]], - istream: Union[None, BinaryIO] = None, - with_extended_output: bool = False, - with_exceptions: bool = True, - as_process: bool = False, - output_stream: Union[None, BinaryIO] = None, - stdout_as_string: bool = True, - kill_after_timeout: Union[None, float] = None, - with_stdout: bool = True, - universal_newlines: bool = False, - shell: Union[None, bool] = None, - env: Union[None, Mapping[str, str]] = None, - max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, - strip_newline_in_stdout: bool = True, - **subprocess_kwargs: Any, - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: - R"""Handle executing the command, and consume and return the returned - information (stdout). - - :param command: - The command argument list to execute. - It should be a sequence of program arguments, or a string. The - program to execute is the first item in the args sequence or string. - - :param istream: - Standard input filehandle passed to :class:`subprocess.Popen`. - - :param with_extended_output: - Whether to return a (status, stdout, stderr) tuple. - - :param with_exceptions: - Whether to raise an exception when git returns a non-zero status. - - :param as_process: - Whether to return the created process instance directly from which - streams can be read on demand. This will render `with_extended_output` - and `with_exceptions` ineffective - the caller will have to deal with - the details. It is important to note that the process will be placed - into an :class:`AutoInterrupt` wrapper that will interrupt the process - once it goes out of scope. If you use the command in iterators, you - should pass the whole process instance instead of a single stream. - - :param output_stream: - If set to a file-like object, data produced by the git command will be - copied to the given stream instead of being returned as a string. - This feature only has any effect if `as_process` is ``False``. - - :param stdout_as_string: - If ``False``, the command's standard output will be bytes. Otherwise, it - will be decoded into a string using the default encoding (usually UTF-8). - The latter can fail, if the output contains binary data. - - :param kill_after_timeout: - Specifies a timeout in seconds for the git command, after which the process - should be killed. This will have no effect if `as_process` is set to - ``True``. It is set to ``None`` by default and will let the process run - until the timeout is explicitly specified. Uses of this feature should be - carefully considered, due to the following limitations: - - 1. This feature is not supported at all on Windows. - 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to - enumerate child processes, which is available on most GNU/Linux systems - but not most others. - 3. Deeper descendants do not receive signals, though they may sometimes - terminate as a consequence of their parent processes being killed. - 4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side - effects on a repository. For example, stale locks in case of - :manpage:`git-gc(1)` could render the repository incapable of accepting - changes until the lock is manually removed. - - :param with_stdout: - If ``True``, default ``True``, we open stdout on the created process. - - :param universal_newlines: - If ``True``, pipes will be opened as text, and lines are split at all known - line endings. - - :param shell: - Whether to invoke commands through a shell - (see :class:`Popen(..., shell=True) `). - If this is not ``None``, it overrides :attr:`USE_SHELL`. - - Passing ``shell=True`` to this or any other GitPython function should be - avoided, as it is unsafe under most circumstances. This is because it is - typically not feasible to fully consider and account for the effect of shell - expansions, especially when passing ``shell=True`` to other methods that - forward it to :meth:`Git.execute`. Passing ``shell=True`` is also no longer - needed (nor useful) to work around any known operating system specific - issues. - - :param env: - A dictionary of environment variables to be passed to - :class:`subprocess.Popen`. - - :param max_chunk_size: - Maximum number of bytes in one chunk of data passed to the `output_stream` - in one invocation of its ``write()`` method. If the given number is not - positive then the default value is used. - - :param strip_newline_in_stdout: - Whether to strip the trailing ``\n`` of the command stdout. - - :param subprocess_kwargs: - Keyword arguments to be passed to :class:`subprocess.Popen`. Please note - that some of the valid kwargs are already set by this method; the ones you - specify may not be the same ones. - - :return: - * str(output), if `extended_output` is ``False`` (Default) - * tuple(int(status), str(stdout), str(stderr)), - if `extended_output` is ``True`` - - If `output_stream` is ``True``, the stdout value will be your output stream: - - * output_stream, if `extended_output` is ``False`` - * tuple(int(status), output_stream, str(stderr)), - if `extended_output` is ``True`` - - Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent - output regardless of system language. - - :raise git.exc.GitCommandError: - - :note: - If you add additional keyword arguments to the signature of this method, you - must update the ``execute_kwargs`` variable housed in this module. - """ - # Remove password for the command if present. - redacted_command = remove_password_if_present(command) - if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): - _logger.info(" ".join(redacted_command)) - - # Allow the user to have the command executed in their working dir. - try: - cwd = self._working_dir or os.getcwd() # type: Union[None, str] - if not os.access(str(cwd), os.X_OK): - cwd = None - except FileNotFoundError: - cwd = None - - # Start the process. - inline_env = env - env = os.environ.copy() - # Attempt to force all output to plain ASCII English, which is what some parsing - # code may expect. - # According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well - # just to be sure. - env["LANGUAGE"] = "C" - env["LC_ALL"] = "C" - env.update(self._environment) - if inline_env is not None: - env.update(inline_env) - - if sys.platform == "win32": - if kill_after_timeout is not None: - raise GitCommandError( - redacted_command, - '"kill_after_timeout" feature is not supported on Windows.', - ) - cmd_not_found_exception = OSError - else: - cmd_not_found_exception = FileNotFoundError - # END handle - - stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") - if shell is None: - # Get the value of USE_SHELL with no deprecation warning. Do this without - # warnings.catch_warnings, to avoid a race condition with application code - # configuring warnings. The value could be looked up in type(self).__dict__ - # or Git.__dict__, but those can break under some circumstances. This works - # the same as self.USE_SHELL in more situations; see Git.__getattribute__. - shell = super().__getattribute__("USE_SHELL") - _logger.debug( - "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)", - redacted_command, - cwd, - "" if istream else "None", - shell, - universal_newlines, - ) - try: - proc = safer_popen( - command, - env=env, - cwd=cwd, - bufsize=-1, - stdin=(istream or DEVNULL), - stderr=PIPE, - stdout=stdout_sink, - shell=shell, - universal_newlines=universal_newlines, - encoding=defenc if universal_newlines else None, - **subprocess_kwargs, - ) - except cmd_not_found_exception as err: - raise GitCommandNotFound(redacted_command, err) from err - else: - # Replace with a typeguard for Popen[bytes]? - proc.stdout = cast(BinaryIO, proc.stdout) - proc.stderr = cast(BinaryIO, proc.stderr) - - if as_process: - return self.AutoInterrupt(proc, command) - - if sys.platform != "win32" and kill_after_timeout is not None: - # Help mypy figure out this is not None even when used inside communicate(). - timeout = kill_after_timeout - - def kill_process(pid: int) -> None: - """Callback to kill a process. - - This callback implementation would be ineffective and unsafe on Windows. - """ - p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE) - child_pids = [] - if p.stdout is not None: - for line in p.stdout: - if len(line.split()) > 0: - local_pid = (line.split())[0] - if local_pid.isdigit(): - child_pids.append(int(local_pid)) - try: - os.kill(pid, signal.SIGKILL) - for child_pid in child_pids: - try: - os.kill(child_pid, signal.SIGKILL) - except OSError: - pass - # Tell the main routine that the process was killed. - kill_check.set() - except OSError: - # It is possible that the process gets completed in the duration - # after timeout happens and before we try to kill the process. - pass - return - - def communicate() -> Tuple[AnyStr, AnyStr]: - watchdog.start() - out, err = proc.communicate() - watchdog.cancel() - if kill_check.is_set(): - err = 'Timeout: the command "%s" did not complete in %d ' "secs." % ( - " ".join(redacted_command), - timeout, - ) - if not universal_newlines: - err = err.encode(defenc) - return out, err - - # END helpers - - kill_check = threading.Event() - watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,)) - else: - communicate = proc.communicate - - # Wait for the process to return. - status = 0 - stdout_value: Union[str, bytes] = b"" - stderr_value: Union[str, bytes] = b"" - newline = "\n" if universal_newlines else b"\n" - try: - if output_stream is None: - stdout_value, stderr_value = communicate() - # Strip trailing "\n". - if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type] - stdout_value = stdout_value[:-1] - if stderr_value.endswith(newline): # type: ignore[arg-type] - stderr_value = stderr_value[:-1] - - status = proc.returncode - else: - max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE - stream_copy(proc.stdout, output_stream, max_chunk_size) - stdout_value = proc.stdout.read() - stderr_value = proc.stderr.read() - # Strip trailing "\n". - if stderr_value.endswith(newline): # type: ignore[arg-type] - stderr_value = stderr_value[:-1] - status = proc.wait() - # END stdout handling - finally: - proc.stdout.close() - proc.stderr.close() - - if self.GIT_PYTHON_TRACE == "full": - cmdstr = " ".join(redacted_command) - - def as_text(stdout_value: Union[bytes, str]) -> str: - return not output_stream and safe_decode(stdout_value) or "" - - # END as_text - - if stderr_value: - _logger.info( - "%s -> %d; stdout: '%s'; stderr: '%s'", - cmdstr, - status, - as_text(stdout_value), - safe_decode(stderr_value), - ) - elif stdout_value: - _logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value)) - else: - _logger.info("%s -> %d", cmdstr, status) - # END handle debug printing - - if with_exceptions and status != 0: - raise GitCommandError(redacted_command, status, stderr_value, stdout_value) - - if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream. - stdout_value = safe_decode(stdout_value) - - # Allow access to the command's status code. - if with_extended_output: - return (status, stdout_value, safe_decode(stderr_value)) - else: - return stdout_value - - def environment(self) -> Dict[str, str]: - return self._environment - - def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: - """Set environment variables for future git invocations. Return all changed - values in a format that can be passed back into this function to revert the - changes. - - Examples:: - - old_env = self.update_environment(PWD='/tmp') - self.update_environment(**old_env) - - :param kwargs: - Environment variables to use for git processes. - - :return: - Dict that maps environment variables to their old values - """ - old_env = {} - for key, value in kwargs.items(): - # Set value if it is None. - if value is not None: - old_env[key] = self._environment.get(key) - self._environment[key] = value - # Remove key from environment if its value is None. - elif key in self._environment: - old_env[key] = self._environment[key] - del self._environment[key] - return old_env - - @contextlib.contextmanager - def custom_environment(self, **kwargs: Any) -> Iterator[None]: - """A context manager around the above :meth:`update_environment` method to - restore the environment back to its previous state after operation. - - Examples:: - - with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'): - repo.remotes.origin.fetch() - - :param kwargs: - See :meth:`update_environment`. - """ - old_env = self.update_environment(**kwargs) - try: - yield - finally: - self.update_environment(**old_env) - - def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]: - if len(name) == 1: - if value is True: - return ["-%s" % name] - elif value not in (False, None): - if split_single_char_options: - return ["-%s" % name, "%s" % value] - else: - return ["-%s%s" % (name, value)] - else: - if value is True: - return ["--%s" % dashify(name)] - elif value is not False and value is not None: - return ["--%s=%s" % (dashify(name), value)] - return [] - - def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: - """Transform Python-style kwargs into git command line options.""" - args = [] - for k, v in kwargs.items(): - if isinstance(v, (list, tuple)): - for value in v: - args += self.transform_kwarg(k, value, split_single_char_options) - else: - args += self.transform_kwarg(k, v, split_single_char_options) - return args - - @classmethod - def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: - outlist = [] - if isinstance(arg_list, (list, tuple)): - for arg in arg_list: - outlist.extend(cls._unpack_args(arg)) - else: - outlist.append(str(arg_list)) - - return outlist - - def __call__(self, **kwargs: Any) -> "Git": - """Specify command line options to the git executable for a subcommand call. - - :param kwargs: - A dict of keyword arguments. - These arguments are passed as in :meth:`_call_process`, but will be passed - to the git command rather than the subcommand. - - Examples:: - - git(work_tree='/tmp').difftool() - """ - self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) - return self - - @overload - def _call_process( - self, method: str, *args: None, **kwargs: None - ) -> str: ... # If no args were given, execute the call with all defaults. - - @overload - def _call_process( - self, - method: str, - istream: int, - as_process: Literal[True], - *args: Any, - **kwargs: Any, - ) -> "Git.AutoInterrupt": ... - - @overload - def _call_process( - self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ... - - def _call_process( - self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: - """Run the given git command with the specified arguments and return the result - as a string. - - :param method: - The command. Contained ``_`` characters will be converted to hyphens, such - as in ``ls_files`` to call ``ls-files``. - - :param args: - The list of arguments. If ``None`` is included, it will be pruned. - This allows your commands to call git more conveniently, as ``None`` is - realized as non-existent. - - :param kwargs: - Contains key-values for the following: - - - The :meth:`execute()` kwds, as listed in ``execute_kwargs``. - - "Command options" to be converted by :meth:`transform_kwargs`. - - The ``insert_kwargs_after`` key which its value must match one of - ``*args``. - - It also contains any command options, to be appended after the matched arg. - - Examples:: - - git.rev_list('master', max_count=10, header=True) - - turns into:: - - git rev-list max-count 10 --header master - - :return: - Same as :meth:`execute`. If no args are given, used :meth:`execute`'s - default (especially ``as_process = False``, ``stdout_as_string = True``) and - return :class:`str`. - """ - # Handle optional arguments prior to calling transform_kwargs. - # Otherwise these'll end up in args, which is bad. - exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} - opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs} - - insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None) - - # Prepare the argument list. - - opt_args = self.transform_kwargs(**opts_kwargs) - ext_args = self._unpack_args([a for a in args if a is not None]) - - if insert_after_this_arg is None: - args_list = opt_args + ext_args - else: - try: - index = ext_args.index(insert_after_this_arg) - except ValueError as err: - raise ValueError( - "Couldn't find argument '%s' in args %s to insert cmd options after" - % (insert_after_this_arg, str(ext_args)) - ) from err - # END handle error - args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :] - # END handle opts_kwargs - - call = [self.GIT_PYTHON_GIT_EXECUTABLE] - - # Add persistent git options. - call.extend(self._persistent_git_options) - - # Add the git options, then reset to empty to avoid side effects. - call.extend(self._git_options) - self._git_options = () - - call.append(dashify(method)) - call.extend(args_list) - - return self.execute(call, **exec_kwargs) - - def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: - """ - :param header_line: - A line of the form:: - - type_string size_as_int - - :return: - (hex_sha, type_string, size_as_int) - - :raise ValueError: - If the header contains indication for an error due to incorrect input sha. - """ - tokens = header_line.split() - if len(tokens) != 3: - if not tokens: - err_msg = ( - f"SHA is empty, possible dubious ownership in the repository " - f"""at {self._working_dir}.\n If this is unintended run:\n\n """ - f""" "git config --global --add safe.directory {self._working_dir}" """ - ) - raise ValueError(err_msg) - else: - raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) - # END handle actual return value - # END error handling - - if len(tokens[0]) != 40: - raise ValueError("Failed to parse header: %r" % header_line) - return (tokens[0], tokens[1], int(tokens[2])) - - def _prepare_ref(self, ref: AnyStr) -> bytes: - # Required for command to separate refs on stdin, as bytes. - if isinstance(ref, bytes): - # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text. - refstr: str = ref.decode("ascii") - elif not isinstance(ref, str): - refstr = str(ref) # Could be ref-object. - else: - refstr = ref - - if not refstr.endswith("\n"): - refstr += "\n" - return refstr.encode(defenc) - - def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt": - cur_val = getattr(self, attr_name) - if cur_val is not None: - return cur_val - - options = {"istream": PIPE, "as_process": True} - options.update(kwargs) - - cmd = self._call_process(cmd_name, *args, **options) - setattr(self, attr_name, cmd) - cmd = cast("Git.AutoInterrupt", cmd) - return cmd - - def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]: - if cmd.stdin and cmd.stdout: - cmd.stdin.write(self._prepare_ref(ref)) - cmd.stdin.flush() - return self._parse_object_header(cmd.stdout.readline()) - else: - raise ValueError("cmd stdin was empty") - - def get_object_header(self, ref: str) -> Tuple[str, str, int]: - """Use this method to quickly examine the type and size of the object behind the - given ref. - - :note: - The method will only suffer from the costs of command invocation once and - reuses the command in subsequent calls. - - :return: - (hexsha, type_string, size_as_int) - """ - cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) - return self.__get_object_header(cmd, ref) - - def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: - """Similar to :meth:`get_object_header`, but returns object data as well. - - :return: - (hexsha, type_string, size_as_int, data_string) - - :note: - Not threadsafe. - """ - hexsha, typename, size, stream = self.stream_object_data(ref) - data = stream.read(size) - del stream - return (hexsha, typename, size, data) - - def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]: - """Similar to :meth:`get_object_data`, but returns the data as a stream. - - :return: - (hexsha, type_string, size_as_int, stream) - - :note: - This method is not threadsafe. You need one independent :class:`Git` - instance per thread to be safe! - """ - cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True) - hexsha, typename, size = self.__get_object_header(cmd, ref) - cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() - return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout)) - - def clear_cache(self) -> "Git": - """Clear all kinds of internal caches to release resources. - - Currently persistent commands will be interrupted. - - :return: - self - """ - for cmd in (self.cat_file_all, self.cat_file_header): - if cmd: - cmd.__del__() - - self.cat_file_all = None - self.cat_file_header = None - return self From fe8416f52bf6978a69b494e505095d7b80054d53 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:51 -0800 Subject: [PATCH 007/197] Modified by www.SourceFiles.app --- FUNDING.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 FUNDING.json diff --git a/FUNDING.json b/FUNDING.json deleted file mode 100644 index bf3faa662..000000000 --- a/FUNDING.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "drips": { - "ethereum": { - "ownedBy": "0xD0d4dCFc194ec24bCc777e635289e0b10E1a7b87" - } - } -} From 9e2dbd8695f51eb54b781f5c37b0b9186335b7c8 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:52 -0800 Subject: [PATCH 008/197] Modified by www.SourceFiles.app --- fuzzing/LICENSE-BSD | 1 - 1 file changed, 1 deletion(-) delete mode 120000 fuzzing/LICENSE-BSD diff --git a/fuzzing/LICENSE-BSD b/fuzzing/LICENSE-BSD deleted file mode 120000 index ea5b60640..000000000 --- a/fuzzing/LICENSE-BSD +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file From b88637cbf50c6ff340568167405751bab7afb39d Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:53 -0800 Subject: [PATCH 009/197] Modified by www.SourceFiles.app --- fuzzing/README.md | 226 ---------------------------------------------- 1 file changed, 226 deletions(-) delete mode 100644 fuzzing/README.md diff --git a/fuzzing/README.md b/fuzzing/README.md deleted file mode 100644 index 286f529eb..000000000 --- a/fuzzing/README.md +++ /dev/null @@ -1,226 +0,0 @@ -# Fuzzing GitPython - -[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/gitpython.svg)][oss-fuzz-issue-tracker] - -This directory contains files related to GitPython's suite of fuzz tests that are executed daily on automated -infrastructure provided by [OSS-Fuzz][oss-fuzz-repo]. This document aims to provide necessary information for working -with fuzzing in GitPython. - -The latest details regarding OSS-Fuzz test status, including build logs and coverage reports, is available -on [the Open Source Fuzzing Introspection website](https://introspector.oss-fuzz.com/project-profile?project=gitpython). - -## How to Contribute - -There are many ways to contribute to GitPython's fuzzing efforts! Contributions are welcomed through issues, -discussions, or pull requests on this repository. - -Areas that are particularly appreciated include: - -- **Tackling the existing backlog of open issues**. While fuzzing is an effective way to identify bugs, that information - isn't useful unless they are fixed. If you are not sure where to start, the issues tab is a great place to get ideas! -- **Improvements to this (or other) documentation** make it easier for new contributors to get involved, so even small - improvements can have a large impact over time. If you see something that could be made easier by a documentation - update of any size, please consider suggesting it! - -For everything else, such as expanding test coverage, optimizing test performance, or enhancing error detection -capabilities, jump into the "Getting Started" section below. - -## Getting Started with Fuzzing GitPython - -> [!TIP] -> **New to fuzzing or unfamiliar with OSS-Fuzz?** -> -> These resources are an excellent place to start: -> -> - [OSS-Fuzz documentation][oss-fuzz-docs] - Continuous fuzzing service for open source software. -> - [Google/fuzzing][google-fuzzing-repo] - Tutorials, examples, discussions, research proposals, and other resources - related to fuzzing. -> - [CNCF Fuzzing Handbook](https://github.com/cncf/tag-security/blob/main/security-fuzzing-handbook/handbook-fuzzing.pdf) - - A comprehensive guide for fuzzing open source software. -> - [Efficient Fuzzing Guide by The Chromium Project](https://chromium.googlesource.com/chromium/src/+/main/testing/libfuzzer/efficient_fuzzing.md) - - Explores strategies to enhance the effectiveness of your fuzz tests, recommended for those looking to optimize their - testing efforts. - -### Setting Up Your Local Environment - -Before contributing to fuzzing efforts, ensure Python and Docker are installed on your machine. Docker is required for -running fuzzers in containers provided by OSS-Fuzz and for safely executing test files directly. [Install Docker](https://docs.docker.com/get-docker/) following the official guide if you do not already have it. - -### Understanding Existing Fuzz Targets - -Review the `fuzz-targets/` directory to familiarize yourself with how existing tests are implemented. See -the [Files & Directories Overview](#files--directories-overview) for more details on the directory structure. - -### Contributing to Fuzz Tests - -Start by reviewing the [Atheris documentation][atheris-repo] and the section -on [Running Fuzzers Locally](#running-fuzzers-locally) to begin writing or improving fuzz tests. - -## Files & Directories Overview - -The `fuzzing/` directory is organized into three key areas: - -### Fuzz Targets (`fuzz-targets/`) - -Contains Python files for each fuzz test. - -**Things to Know**: - -- Each fuzz test targets a specific part of GitPython's functionality. -- Test files adhere to the naming convention: `fuzz_.py`, where `` indicates the - functionality targeted by the test. -- Any functionality that involves performing operations on input data is a possible candidate for fuzz testing, but - features that involve processing untrusted user input or parsing operations are typically going to be the most - interesting. -- The goal of these tests is to identify previously unknown or unexpected error cases caused by a given input. For that - reason, fuzz tests should gracefully handle anticipated exception cases with a `try`/`except` block to avoid false - positives that halt the fuzzing engine. - -### OSS-Fuzz Scripts (`oss-fuzz-scripts/`) - -Includes scripts for building and integrating fuzz targets with OSS-Fuzz: - -- **`container-environment-bootstrap.sh`** - Sets up the execution environment. It is responsible for fetching default - dictionary entries and ensuring all required build dependencies are installed and up-to-date. -- **`build.sh`** - Executed within the Docker container, this script builds fuzz targets with necessary instrumentation - and prepares seed corpora and dictionaries for use. - -**Where to learn more:** - -- [OSS-Fuzz documentation on the build.sh](https://google.github.io/oss-fuzz/getting-started/new-project-guide/#buildsh) -- [See GitPython's build.sh and Dockerfile in the OSS-Fuzz repository](https://github.com/google/oss-fuzz/tree/master/projects/gitpython) - -### Local Development Helpers (`local-dev-helpers/`) - -Contains tools to make local development tasks easier. -See [the "Running Fuzzers Locally" section below](#running-fuzzers-locally) for further documentation and use cases related to files found here. - -## Running Fuzzers Locally - -> [!WARNING] -> **Some fuzz targets in this repository write to the filesystem** during execution. -> For that reason, it is strongly recommended to **always use Docker when executing fuzz targets**, even when it may be -> possible to do so without it. -> -> Although [I/O operations such as writing to disk are not considered best practice](https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md#io), the current implementation of at least one test requires it. -> See [the "Setting Up Your Local Environment" section above](#setting-up-your-local-environment) if you do not already have Docker installed on your machine. -> -> PRs that replace disk I/O with in-memory alternatives are very much welcomed! - -### Direct Execution of Fuzz Targets - -Directly executing fuzz targets allows for quick iteration and testing of changes which can be helpful during early -development of new fuzz targets or for validating changes made to an existing test. -The [Dockerfile](./local-dev-helpers/Dockerfile) located in the `local-dev-helpers/` subdirectory provides a lightweight -container environment preconfigured with [Atheris][atheris-repo] that makes it easy to execute a fuzz target directly. - -**From the root directory of your GitPython repository clone**: - -1. Build the local development helper image: - -```shell -docker build -f fuzzing/local-dev-helpers/Dockerfile -t gitpython-fuzzdev . -``` - -2. Then execute a fuzz target inside the image, for example: - -```shell - docker run -it -v "$PWD":/src gitpython-fuzzdev python fuzzing/fuzz-targets/fuzz_config.py -atheris_runs=10000 -``` - -The above command executes [`fuzz_config.py`](./fuzz-targets/fuzz_config.py) and exits after `10000` runs, or earlier if -the fuzzer finds an error. - -Docker CLI's `-v` flag specifies a volume mount in Docker that maps the directory in which the command is run (which -should be the root directory of your local GitPython clone) to a directory inside the container, so any modifications -made between invocations will be reflected immediately without the need to rebuild the image each time. - -### Running OSS-Fuzz Locally - -This approach uses Docker images provided by OSS-Fuzz for building and running fuzz tests locally. It offers -comprehensive features but requires a local clone of the OSS-Fuzz repository and sufficient disk space for Docker -containers. - -#### Build the Execution Environment - -Clone the OSS-Fuzz repository and prepare the Docker environment: - -```shell -git clone --depth 1 https://github.com/google/oss-fuzz.git oss-fuzz -cd oss-fuzz -python infra/helper.py build_image gitpython -python infra/helper.py build_fuzzers --sanitizer address gitpython -``` - -> [!TIP] -> The `build_fuzzers` command above accepts a local file path pointing to your GitPython repository clone as the last -> argument. -> This makes it easy to build fuzz targets you are developing locally in this repository without changing anything in -> the OSS-Fuzz repo! -> For example, if you have cloned this repository (or a fork of it) into: `~/code/GitPython` -> Then running this command would build new or modified fuzz targets using the `~/code/GitPython/fuzzing/fuzz-targets` -> directory: -> ```shell -> python infra/helper.py build_fuzzers --sanitizer address gitpython ~/code/GitPython -> ``` - -Verify the build of your fuzzers with the optional `check_build` command: - -```shell -python infra/helper.py check_build gitpython -``` - -#### Run a Fuzz Target - -Setting an environment variable for the fuzz target argument of the execution command makes it easier to quickly select -a different target between runs: - -```shell -# specify the fuzz target without the .py extension: -export FUZZ_TARGET=fuzz_config -``` - -Execute the desired fuzz target: - -```shell -python infra/helper.py run_fuzzer gitpython $FUZZ_TARGET -- -max_total_time=60 -print_final_stats=1 -``` - -> [!TIP] -> In the example above, the "`-- -max_total_time=60 -print_final_stats=1`" portion of the command is optional but quite -> useful. -> -> Every argument provided after "`--`" in the above command is passed to the fuzzing engine directly. In this case: -> - `-max_total_time=60` tells the LibFuzzer to stop execution after 60 seconds have elapsed. -> - `-print_final_stats=1` tells the LibFuzzer to print a summary of useful metrics about the target run upon - completion. -> -> But almost any [LibFuzzer option listed in the documentation](https://llvm.org/docs/LibFuzzer.html#options) should -> work as well. - -#### Next Steps - -For detailed instructions on advanced features like reproducing OSS-Fuzz issues or using the Fuzz Introspector, refer -to [the official OSS-Fuzz documentation][oss-fuzz-docs]. - -## LICENSE - -All files located within the `fuzzing/` directory are subject to [the same license](../LICENSE) -as [the other files in this repository](../README.md#license) with one exception: - -[`fuzz_config.py`](./fuzz-targets/fuzz_config.py) was migrated to this repository from the OSS-Fuzz project's repository -where it was originally created. As such, [`fuzz_config.py`](./fuzz-targets/fuzz_config.py) retains its original license -and copyright notice (Apache License, Version 2.0 and Copyright 2023 Google LLC respectively) as in a header -comment, followed by a notice stating that it has have been modified contributors to GitPython. -[LICENSE-APACHE](./LICENSE-APACHE) contains the original license used by the OSS-Fuzz project repository at the time the -file was migrated. - -[oss-fuzz-repo]: https://github.com/google/oss-fuzz - -[oss-fuzz-docs]: https://google.github.io/oss-fuzz - -[oss-fuzz-issue-tracker]: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:gitpython - -[google-fuzzing-repo]: https://github.com/google/fuzzing - -[atheris-repo]: https://github.com/google/atheris From 189147c7c6d4847f04b572677d5a666917308fe9 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:54 -0800 Subject: [PATCH 010/197] Modified by www.SourceFiles.app --- fuzzing/fuzz-targets/fuzz_blob.py | 40 ------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 fuzzing/fuzz-targets/fuzz_blob.py diff --git a/fuzzing/fuzz-targets/fuzz_blob.py b/fuzzing/fuzz-targets/fuzz_blob.py deleted file mode 100644 index ce888e85f..000000000 --- a/fuzzing/fuzz-targets/fuzz_blob.py +++ /dev/null @@ -1,40 +0,0 @@ -import atheris -import sys -import os -import tempfile - -if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): - path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) - os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary - -with atheris.instrument_imports(): - import git - - -def TestOneInput(data): - fdp = atheris.FuzzedDataProvider(data) - - with tempfile.TemporaryDirectory() as temp_dir: - repo = git.Repo.init(path=temp_dir) - binsha = fdp.ConsumeBytes(20) - mode = fdp.ConsumeInt(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())) - path = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes()) - - try: - blob = git.Blob(repo, binsha, mode, path) - except AssertionError as e: - if "Require 20 byte binary sha, got" in str(e): - return -1 - else: - raise e - - _ = blob.mime_type - - -def main(): - atheris.Setup(sys.argv, TestOneInput) - atheris.Fuzz() - - -if __name__ == "__main__": - main() From 8238374956ac9cf149e33e43496f5796ac3133e6 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:54 -0800 Subject: [PATCH 011/197] Modified by www.SourceFiles.app --- fuzzing/fuzz-targets/fuzz_config.py | 57 ----------------------------- 1 file changed, 57 deletions(-) delete mode 100644 fuzzing/fuzz-targets/fuzz_config.py diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py deleted file mode 100644 index 4eddc32ff..000000000 --- a/fuzzing/fuzz-targets/fuzz_config.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -############################################################################### -# Note: This file has been modified by contributors to GitPython. -# The original state of this file may be referenced here: -# https://github.com/google/oss-fuzz/commit/f26f254558fc48f3c9bc130b10507386b94522da -############################################################################### -import atheris -import sys -import io -import os -from configparser import MissingSectionHeaderError, ParsingError - -if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): - path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) - os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary - -with atheris.instrument_imports(): - import git - - -def TestOneInput(data): - sio = io.BytesIO(data) - sio.name = "/tmp/fuzzconfig.config" - git_config = git.GitConfigParser(sio) - try: - git_config.read() - except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError): - return -1 # Reject inputs raising expected exceptions - except ValueError as e: - if "embedded null byte" in str(e): - # The `os.path.expanduser` function, which does not accept strings - # containing null bytes might raise this. - return -1 - else: - raise e # Raise unanticipated exceptions as they might be bugs - - -def main(): - atheris.Setup(sys.argv, TestOneInput) - atheris.Fuzz() - - -if __name__ == "__main__": - main() From 490d9de8d81927b3940fcf68280dbe77894d70e0 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:55 -0800 Subject: [PATCH 012/197] Modified by www.SourceFiles.app --- git/compat.py | 165 -------------------------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 git/compat.py diff --git a/git/compat.py b/git/compat.py deleted file mode 100644 index d7d9a55a9..000000000 --- a/git/compat.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under the -# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ - -"""Utilities to help provide compatibility with Python 3. - -This module exists for historical reasons. Code outside GitPython may make use of public -members of this module, but is unlikely to benefit from doing so. GitPython continues to -use some of these utilities, in some cases for compatibility across different platforms. -""" - -import locale -import os -import sys -import warnings - -from gitdb.utils.encoding import force_bytes, force_text # noqa: F401 - -# typing -------------------------------------------------------------------- - -from typing import ( - Any, # noqa: F401 - AnyStr, - Dict, # noqa: F401 - IO, # noqa: F401 - List, - Optional, - TYPE_CHECKING, - Tuple, # noqa: F401 - Type, # noqa: F401 - Union, - overload, -) - -# --------------------------------------------------------------------------- - - -_deprecated_platform_aliases = { - "is_win": os.name == "nt", - "is_posix": os.name == "posix", - "is_darwin": sys.platform == "darwin", -} - - -def _getattr(name: str) -> Any: - try: - value = _deprecated_platform_aliases[name] - except KeyError: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None - - warnings.warn( - f"{__name__}.{name} and other is_ aliases are deprecated. " - "Write the desired os.name or sys.platform check explicitly instead.", - DeprecationWarning, - stacklevel=2, - ) - return value - - -if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes. - __getattr__ = _getattr - - -def __dir__() -> List[str]: - return [*globals(), *_deprecated_platform_aliases] - - -is_win: bool -"""Deprecated alias for ``os.name == "nt"`` to check for native Windows. - -This is deprecated because it is clearer to write out :attr:`os.name` or -:attr:`sys.platform` checks explicitly, especially in cases where it matters which is -used. - -:note: - ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To detect - Cygwin, use ``sys.platform == "cygwin"``. -""" - -is_posix: bool -"""Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems. - -This is deprecated because it clearer to write out :attr:`os.name` or -:attr:`sys.platform` checks explicitly, especially in cases where it matters which is -used. - -:note: - For POSIX systems, more detailed information is available in :attr:`sys.platform`, - while :attr:`os.name` is always ``"posix"`` on such systems, including macOS - (Darwin). -""" - -is_darwin: bool -"""Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin). - -This is deprecated because it clearer to write out :attr:`os.name` or -:attr:`sys.platform` checks explicitly. - -:note: - For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while - ``sys.platform == "darwin"``. -""" - -defenc = sys.getfilesystemencoding() -"""The encoding used to convert between Unicode and bytes filenames.""" - - -@overload -def safe_decode(s: None) -> None: ... - - -@overload -def safe_decode(s: AnyStr) -> str: ... - - -def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: - """Safely decode a binary string to Unicode.""" - if isinstance(s, str): - return s - elif isinstance(s, bytes): - return s.decode(defenc, "surrogateescape") - elif s is None: - return None - else: - raise TypeError("Expected bytes or text, but got %r" % (s,)) - - -@overload -def safe_encode(s: None) -> None: ... - - -@overload -def safe_encode(s: AnyStr) -> bytes: ... - - -def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: - """Safely encode a binary string to Unicode.""" - if isinstance(s, str): - return s.encode(defenc) - elif isinstance(s, bytes): - return s - elif s is None: - return None - else: - raise TypeError("Expected bytes or text, but got %r" % (s,)) - - -@overload -def win_encode(s: None) -> None: ... - - -@overload -def win_encode(s: AnyStr) -> bytes: ... - - -def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: - """Encode Unicode strings for process arguments on Windows.""" - if isinstance(s, str): - return s.encode(locale.getpreferredencoding(False)) - elif isinstance(s, bytes): - return s - elif s is not None: - raise TypeError("Expected bytes or text, but got %r" % (s,)) - return None From 95996555fefb11bf0cc894257893c49baff86445 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:55 -0800 Subject: [PATCH 013/197] Modified by www.SourceFiles.app --- fuzzing/fuzz-targets/fuzz_diff.py | 86 ------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 fuzzing/fuzz-targets/fuzz_diff.py diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py deleted file mode 100644 index d4bd68b57..000000000 --- a/fuzzing/fuzz-targets/fuzz_diff.py +++ /dev/null @@ -1,86 +0,0 @@ -import sys -import os -import io -import tempfile -from binascii import Error as BinasciiError - -import atheris - -if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): - path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) - os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary - -with atheris.instrument_imports(): - from git import Repo, Diff - - -class BytesProcessAdapter: - """Allows bytes to be used as process objects returned by subprocess.Popen.""" - - @atheris.instrument_func - def __init__(self, input_string): - self.stdout = io.BytesIO(input_string) - self.stderr = io.BytesIO() - - @atheris.instrument_func - def wait(self): - return 0 - - poll = wait - - -@atheris.instrument_func -def TestOneInput(data): - fdp = atheris.FuzzedDataProvider(data) - - with tempfile.TemporaryDirectory() as temp_dir: - repo = Repo.init(path=temp_dir) - try: - diff = Diff( - repo, - a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - a_blob_id=fdp.ConsumeBytes(20), - b_blob_id=fdp.ConsumeBytes(20), - a_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - b_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - new_file=fdp.ConsumeBool(), - deleted_file=fdp.ConsumeBool(), - copied_file=fdp.ConsumeBool(), - raw_rename_from=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - raw_rename_to=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - diff=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), - change_type=fdp.PickValueInList(["A", "D", "C", "M", "R", "T", "U"]), - score=fdp.ConsumeIntInRange(0, fdp.remaining_bytes()), - ) - except BinasciiError: - return -1 - except AssertionError as e: - if "Require 20 byte binary sha, got" in str(e): - return -1 - else: - raise e - - _ = diff.__str__() - _ = diff.a_path - _ = diff.b_path - _ = diff.rename_from - _ = diff.rename_to - _ = diff.renamed_file - - diff_index = diff._index_from_patch_format( - repo, proc=BytesProcessAdapter(fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes()))) - ) - - diff._handle_diff_line( - lines_bytes=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), repo=repo, index=diff_index - ) - - -def main(): - atheris.Setup(sys.argv, TestOneInput) - atheris.Fuzz() - - -if __name__ == "__main__": - main() From 42a8e77350e43b1fbba919e5bf6797012680d449 Mon Sep 17 00:00:00 2001 From: matthew9ilbert Date: Sun, 9 Mar 2025 01:01:55 -0800 Subject: [PATCH 014/197] Modified by www.SourceFiles.app --- git/config.py | 944 -------------------------------------------------- 1 file changed, 944 deletions(-) delete mode 100644 git/config.py diff --git a/git/config.py b/git/config.py deleted file mode 100644 index de3508360..000000000 --- a/git/config.py +++ /dev/null @@ -1,944 +0,0 @@ -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under the -# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ - -"""Parser for reading and writing configuration files.""" - -__all__ = ["GitConfigParser", "SectionConstraint"] - -import abc -import configparser as cp -import fnmatch -from functools import wraps -import inspect -from io import BufferedReader, IOBase -import logging -import os -import os.path as osp -import re -import sys - -from git.compat import defenc, force_text -from git.util import LockFile - -# typing------------------------------------------------------- - -from typing import ( - Any, - Callable, - Generic, - IO, - List, - Dict, - Sequence, - TYPE_CHECKING, - Tuple, - TypeVar, - Union, - cast, -) - -from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T - -if TYPE_CHECKING: - from io import BytesIO - - from git.repo.base import Repo - -T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") -T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool) - -if sys.version_info[:3] < (3, 7, 2): - # typing.Ordereddict not added until Python 3.7.2. - from collections import OrderedDict - - OrderedDict_OMD = OrderedDict -else: - from typing import OrderedDict - - OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] - -# ------------------------------------------------------------- - -_logger = logging.getLogger(__name__) - -CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") -"""The configuration level of a configuration file.""" - -CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") -"""Section pattern to detect conditional includes. - -See: https://git-scm.com/docs/git-config#_conditional_includes -""" - - -class MetaParserBuilder(abc.ABCMeta): # noqa: B024 - """Utility class wrapping base-class methods into decorators that assure read-only - properties.""" - - def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder": - """Equip all base-class methods with a needs_values decorator, and all non-const - methods with a :func:`set_dirty_and_flush_changes` decorator in addition to - that. - """ - kmm = "_mutating_methods_" - if kmm in clsdict: - mutating_methods = clsdict[kmm] - for base in bases: - methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) - for name, method in methods: - if name in clsdict: - continue - method_with_values = needs_values(method) - if name in mutating_methods: - method_with_values = set_dirty_and_flush_changes(method_with_values) - # END mutating methods handling - - clsdict[name] = method_with_values - # END for each name/method pair - # END for each base - # END if mutating methods configuration is set - - new_type = super().__new__(cls, name, bases, clsdict) - return new_type - - -def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: - """Return a method for ensuring we read values (on demand) before we try to access - them.""" - - @wraps(func) - def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: - self.read() - return func(self, *args, **kwargs) - - # END wrapper method - return assure_data_present - - -def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]: - """Return a method that checks whether given non constant function may be called. - - If so, the instance will be set dirty. Additionally, we flush the changes right to - disk. - """ - - def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: - rval = non_const_func(self, *args, **kwargs) - self._dirty = True - self.write() - return rval - - # END wrapper method - flush_changes.__name__ = non_const_func.__name__ - return flush_changes - - -class SectionConstraint(Generic[T_ConfigParser]): - """Constrains a ConfigParser to only option commands which are constrained to - always use the section we have been initialized with. - - It supports all ConfigParser methods that operate on an option. - - :note: - If used as a context manager, will release the wrapped ConfigParser. - """ - - __slots__ = ("_config", "_section_name") - - _valid_attrs_ = ( - "get_value", - "set_value", - "get", - "set", - "getint", - "getfloat", - "getboolean", - "has_option", - "remove_section", - "remove_option", - "options", - ) - - def __init__(self, config: T_ConfigParser, section: str) -> None: - self._config = config - self._section_name = section - - def __del__(self) -> None: - # Yes, for some reason, we have to call it explicitly for it to work in PY3 ! - # Apparently __del__ doesn't get call anymore if refcount becomes 0 - # Ridiculous ... . - self._config.release() - - def __getattr__(self, attr: str) -> Any: - if attr in self._valid_attrs_: - return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) - return super().__getattribute__(attr) - - def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: - """Call the configuration at the given method which must take a section name as - first argument.""" - return getattr(self._config, method)(self._section_name, *args, **kwargs) - - @property - def config(self) -> T_ConfigParser: - """return: ConfigParser instance we constrain""" - return self._config - - def release(self) -> None: - """Equivalent to :meth:`GitConfigParser.release`, which is called on our - underlying parser instance.""" - return self._config.release() - - def __enter__(self) -> "SectionConstraint[T_ConfigParser]": - self._config.__enter__() - return self - - def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: - self._config.__exit__(exception_type, exception_value, traceback) - - -class _OMD(OrderedDict_OMD): - """Ordered multi-dict.""" - - def __setitem__(self, key: str, value: _T) -> None: - super().__setitem__(key, [value]) - - def add(self, key: str, value: Any) -> None: - if key not in self: - super().__setitem__(key, [value]) - return - - super().__getitem__(key).append(value) - - def setall(self, key: str, values: List[_T]) -> None: - super().__setitem__(key, values) - - def __getitem__(self, key: str) -> Any: - return super().__getitem__(key)[-1] - - def getlast(self, key: str) -> Any: - return super().__getitem__(key)[-1] - - def setlast(self, key: str, value: Any) -> None: - if key not in self: - super().__setitem__(key, [value]) - return - - prior = super().__getitem__(key) - prior[-1] = value - - def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: - return super().get(key, [default])[-1] - - def getall(self, key: str) -> List[_T]: - return super().__getitem__(key) - - def items(self) -> List[Tuple[str, _T]]: # type: ignore[override] - """List of (key, last value for key).""" - return [(k, self[k]) for k in self] - - def items_all(self) -> List[Tuple[str, List[_T]]]: - """List of (key, list of values for key).""" - return [(k, self.getall(k)) for k in self] - - -def get_config_path(config_level: Lit_config_levels) -> str: - # We do not support an absolute path of the gitconfig on Windows. - # Use the global config instead. - if sys.platform == "win32" and config_level == "system": - config_level = "global" - - if config_level == "system": - return "/etc/gitconfig" - elif config_level == "user": - config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config") - return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) - elif config_level == "global": - return osp.normpath(osp.expanduser("~/.gitconfig")) - elif config_level == "repository": - raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") - else: - # Should not reach here. Will raise ValueError if does. Static typing will warn - # about missing elifs. - assert_never( # type: ignore[unreachable] - config_level, - ValueError(f"Invalid configuration level: {config_level!r}"), - ) - - -class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): - """Implements specifics required to read git style configuration files. - - This variation behaves much like the :manpage:`git-config(1)` command, such that the - configuration will be read on demand based on the filepath given during - initialization. - - The changes will automatically be written once the instance goes out of scope, but - can be triggered manually as well. - - The configuration file will be locked if you intend to change values preventing - other instances to write concurrently. - - :note: - The config is case-sensitive even when queried, hence section and option names - must match perfectly. - - :note: - If used as a context manager, this will release the locked file. - """ - - # { Configuration - t_lock = LockFile - """The lock type determines the type of lock to use in new configuration readers. - - They must be compatible to the :class:`~git.util.LockFile` interface. - A suitable alternative would be the :class:`~git.util.BlockingLockFile`. - """ - - re_comment = re.compile(r"^\s*[#;]") - # } END configuration - - optvalueonly_source = r"\s*(?P