GitPython 3.1.49__tar.gz → 3.1.51__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {gitpython-3.1.49 → gitpython-3.1.51}/AUTHORS +1 -0
  2. gitpython-3.1.51/CONTRIBUTING.md +46 -0
  3. {gitpython-3.1.49 → gitpython-3.1.51/GitPython.egg-info}/PKG-INFO +1 -1
  4. {gitpython-3.1.49/GitPython.egg-info → gitpython-3.1.51}/PKG-INFO +1 -1
  5. gitpython-3.1.51/VERSION +1 -0
  6. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/changes.rst +26 -1
  7. {gitpython-3.1.49 → gitpython-3.1.51}/git/__init__.py +1 -1
  8. {gitpython-3.1.49 → gitpython-3.1.51}/git/cmd.py +118 -10
  9. {gitpython-3.1.49 → gitpython-3.1.51}/git/config.py +18 -1
  10. {gitpython-3.1.49 → gitpython-3.1.51}/git/diff.py +19 -4
  11. {gitpython-3.1.49 → gitpython-3.1.51}/git/index/base.py +40 -3
  12. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/commit.py +17 -0
  13. {gitpython-3.1.49 → gitpython-3.1.51}/git/remote.py +19 -7
  14. {gitpython-3.1.49 → gitpython-3.1.51}/git/repo/base.py +97 -8
  15. gitpython-3.1.49/CONTRIBUTING.md +0 -15
  16. gitpython-3.1.49/VERSION +0 -1
  17. {gitpython-3.1.49 → gitpython-3.1.51}/CHANGES +0 -0
  18. {gitpython-3.1.49 → gitpython-3.1.51}/GitPython.egg-info/SOURCES.txt +0 -0
  19. {gitpython-3.1.49 → gitpython-3.1.51}/GitPython.egg-info/dependency_links.txt +0 -0
  20. {gitpython-3.1.49 → gitpython-3.1.51}/GitPython.egg-info/not-zip-safe +0 -0
  21. {gitpython-3.1.49 → gitpython-3.1.51}/GitPython.egg-info/requires.txt +0 -0
  22. {gitpython-3.1.49 → gitpython-3.1.51}/GitPython.egg-info/top_level.txt +0 -0
  23. {gitpython-3.1.49 → gitpython-3.1.51}/LICENSE +0 -0
  24. {gitpython-3.1.49 → gitpython-3.1.51}/MANIFEST.in +0 -0
  25. {gitpython-3.1.49 → gitpython-3.1.51}/README.md +0 -0
  26. {gitpython-3.1.49 → gitpython-3.1.51}/doc/.gitignore +0 -0
  27. {gitpython-3.1.49 → gitpython-3.1.51}/doc/Makefile +0 -0
  28. {gitpython-3.1.49 → gitpython-3.1.51}/doc/requirements.txt +0 -0
  29. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/conf.py +0 -0
  30. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/index.rst +0 -0
  31. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/intro.rst +0 -0
  32. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/quickstart.rst +0 -0
  33. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/reference.rst +0 -0
  34. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/roadmap.rst +0 -0
  35. {gitpython-3.1.49 → gitpython-3.1.51}/doc/source/tutorial.rst +0 -0
  36. {gitpython-3.1.49 → gitpython-3.1.51}/git/compat.py +0 -0
  37. {gitpython-3.1.49 → gitpython-3.1.51}/git/db.py +0 -0
  38. {gitpython-3.1.49 → gitpython-3.1.51}/git/exc.py +0 -0
  39. {gitpython-3.1.49 → gitpython-3.1.51}/git/index/__init__.py +0 -0
  40. {gitpython-3.1.49 → gitpython-3.1.51}/git/index/fun.py +0 -0
  41. {gitpython-3.1.49 → gitpython-3.1.51}/git/index/typ.py +0 -0
  42. {gitpython-3.1.49 → gitpython-3.1.51}/git/index/util.py +0 -0
  43. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/__init__.py +0 -0
  44. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/base.py +0 -0
  45. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/blob.py +0 -0
  46. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/fun.py +0 -0
  47. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/submodule/__init__.py +0 -0
  48. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/submodule/base.py +0 -0
  49. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/submodule/root.py +0 -0
  50. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/submodule/util.py +0 -0
  51. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/tag.py +0 -0
  52. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/tree.py +0 -0
  53. {gitpython-3.1.49 → gitpython-3.1.51}/git/objects/util.py +0 -0
  54. {gitpython-3.1.49 → gitpython-3.1.51}/git/py.typed +0 -0
  55. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/__init__.py +0 -0
  56. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/head.py +0 -0
  57. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/log.py +0 -0
  58. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/reference.py +0 -0
  59. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/remote.py +0 -0
  60. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/symbolic.py +0 -0
  61. {gitpython-3.1.49 → gitpython-3.1.51}/git/refs/tag.py +0 -0
  62. {gitpython-3.1.49 → gitpython-3.1.51}/git/repo/__init__.py +0 -0
  63. {gitpython-3.1.49 → gitpython-3.1.51}/git/repo/fun.py +0 -0
  64. {gitpython-3.1.49 → gitpython-3.1.51}/git/types.py +0 -0
  65. {gitpython-3.1.49 → gitpython-3.1.51}/git/util.py +0 -0
  66. {gitpython-3.1.49 → gitpython-3.1.51}/pyproject.toml +0 -0
  67. {gitpython-3.1.49 → gitpython-3.1.51}/requirements.txt +0 -0
  68. {gitpython-3.1.49 → gitpython-3.1.51}/setup.cfg +0 -0
  69. {gitpython-3.1.49 → gitpython-3.1.51}/setup.py +0 -0
  70. {gitpython-3.1.49 → gitpython-3.1.51}/test-requirements.txt +0 -0
@@ -57,5 +57,6 @@ Contributors are:
57
57
  -Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
58
58
  -Gordon Marx
59
59
  -Enji Cooper
60
+ -Harshita Yadav <harshitayadav504 _at_ gmail.com>
60
61
 
61
62
  Portions derived from other open source works and are clearly marked.
@@ -0,0 +1,46 @@
1
+ # How to contribute
2
+
3
+ The following is a short step-by-step rundown of what one typically would do to contribute.
4
+
5
+ - [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub.
6
+ - For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`.
7
+ - Please try to **write a test that fails unless the contribution is present.**
8
+ - Try to avoid massive commits and prefer to take small steps, with one commit for each.
9
+ - Feel free to add yourself to AUTHORS file.
10
+ - Create a pull request.
11
+
12
+ ## Quality expectations
13
+
14
+ Contributions must be made with care and meet the quality bar of the surrounding code.
15
+ That means a change should not leave GitPython worse than it was before: it should be
16
+ readable, maintainable, tested where practical, documented and consistent with the
17
+ existing style and behavior.
18
+
19
+ A contribution that works only narrowly but lowers the quality of the
20
+ codebase may be declined. The maintainers may not always be able to provide
21
+ detailed feedback.
22
+
23
+ ## AI-assisted contributions
24
+
25
+ If AI edits files for you, disclose it in the pull request description and commit
26
+ metadata. Prefer making the agent identity part of the commit, for example by using
27
+ an AI author such as `$agent $version <ai-agent@example.invalid>` or a co-author via
28
+ a `Co-authored-by: <agent-identity>` trailer.
29
+
30
+ Agents operating through a person's GitHub account must identify themselves. For
31
+ example, comments posted by an agent should say so directly with phrases like
32
+ `AI agent on behalf of <person>: ...`.
33
+
34
+ Fully AI-generated comments on pull requests or issues must also be disclosed.
35
+ Undisclosed AI-generated comments may lead to the pull request or issue being closed.
36
+
37
+ AI-assisted proofreading or wording polish does not need disclosure, but it is still
38
+ courteous to mention it when the AI materially influenced the final text.
39
+
40
+ Automated or "full-auto" AI contributions without a human responsible for reviewing
41
+ and standing behind the work may be closed.
42
+
43
+ ## Fuzzing Test Specific Documentation
44
+
45
+ For details related to contributing to the fuzzing test suite and OSS-Fuzz integration, please
46
+ refer to the dedicated [fuzzing README](./fuzzing/README.md).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.49
3
+ Version: 3.1.51
4
4
  Summary: GitPython is a Python library used to interact with Git repositories
5
5
  Home-page: https://github.com/gitpython-developers/GitPython
6
6
  Author: Sebastian Thiel, Michael Trier
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.49
3
+ Version: 3.1.51
4
4
  Summary: GitPython is a Python library used to interact with Git repositories
5
5
  Home-page: https://github.com/gitpython-developers/GitPython
6
6
  Author: Sebastian Thiel, Michael Trier
@@ -0,0 +1 @@
1
+ 3.1.51
@@ -2,11 +2,36 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 3.1.51
6
+ ======
7
+
8
+ This is primarily a security release. It prevents additional argument-injection
9
+ paths that could allow execution of arbitrary commands or writing to arbitrary
10
+ files through unsafe Git options (GHSA-956x-8gvw-wg5v), and closes bypasses of
11
+ the existing protections using abbreviated long options or joined short options
12
+ (GHSA-2f96-g7mh-g2hx).
13
+
14
+ The release also improves support for relative worktree paths and diffs against
15
+ the empty tree, preserves stderr from failed diff processes, supports relative
16
+ configuration include paths, and includes assorted documentation, typing, test,
17
+ and CI improvements.
18
+
19
+ See the following for all changes.
20
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.51
21
+
22
+ 3.1.50
23
+ ======
24
+
25
+ Save setting of configuration values, this time sections as well, as follow-up to 3.1.49.
26
+
27
+ See the following for all changes.
28
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.50
29
+
5
30
  3.1.49
6
31
  ======
7
32
 
8
33
  Save setting of configuration values,
9
- which cuold be used to inject other more configuration.
34
+ which could be used to inject other more configuration.
10
35
 
11
36
  Also more conforming `rev-parse` implementation.
12
37
 
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.49'
89
+ __version__ = '3.1.51'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
@@ -649,6 +649,11 @@ class Git(metaclass=_GitMeta):
649
649
 
650
650
  re_unsafe_protocol = re.compile(r"(.+)::.+")
651
651
 
652
+ unsafe_git_ls_remote_options = [
653
+ # This option allows arbitrary command execution in git-ls-remote.
654
+ "--upload-pack",
655
+ ]
656
+
652
657
  def __getstate__(self) -> Dict[str, Any]:
653
658
  return slots_to_dict(self, exclude=self._excluded_)
654
659
 
@@ -961,17 +966,80 @@ class Git(metaclass=_GitMeta):
961
966
 
962
967
  @classmethod
963
968
  def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
964
- """Check for unsafe options.
965
-
966
- Some options that are passed to ``git <command>`` can be used to execute
967
- arbitrary commands. These are blocked by default.
969
+ """Raise :class:`~git.exc.UnsafeOptionError` for blocked option spellings.
970
+
971
+ In addition to exact matches, this rejects abbreviated long options accepted
972
+ by Git (for example, ``--upl`` for ``--upload-pack``) and unsafe short options
973
+ whose values are joined to the same token, including after clusterable flags
974
+ (for example, ``-uVALUE`` and ``-fuVALUE``).
975
+
976
+ A list containing only bare names is treated as normalized keyword arguments,
977
+ so multi-character names such as ``upload_p`` are checked as long-option
978
+ abbreviations. If any item starts with ``-``, the list is treated as tokenized
979
+ command-line input: bare items can be option values and are not checked as
980
+ abbreviations. Thus ``["--origin", "upload"]`` is allowed. Single-dash options
981
+ use short-option parsing rather than broad prefix matching, preserving safe
982
+ attached values such as ``-oupstream`` and ``-bcurrent``.
983
+
984
+ Some options passed to ``git <command>`` can execute arbitrary commands and
985
+ are therefore blocked by default unless the caller explicitly allows them.
968
986
  """
969
987
  # Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
988
+ # Git accepts any unambiguous prefix of a long option, so an abbreviated
989
+ # spelling such as `--upl` for `--upload-pack` must be rejected too. An
990
+ # option is unsafe if its canonical name is a prefix of any blocked
991
+ # option's canonical name. Only long options and multi-character kwargs
992
+ # can be abbreviations; single-character short options remain exact-match
993
+ # only.
970
994
  canonical_unsafe_options = {cls._canonicalize_option_name(option): option for option in unsafe_options}
995
+ unsafe_short_options = {
996
+ canonical: option
997
+ for canonical, option in canonical_unsafe_options.items()
998
+ if option.startswith("-") and not option.startswith("--") and len(canonical) == 1
999
+ }
1000
+ # These value-less Git flags can be clustered before another short option
1001
+ # (for example, ``-fuVALUE``). Stop at any other character because it may
1002
+ # begin an attached value, as ``o`` does in the safe option ``-oupstream``.
1003
+ clusterable_short_options = frozenset("46flnqsv")
1004
+ options_are_kwargs = all(not option.startswith("-") for option in options)
971
1005
  for option in options:
972
- unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))
1006
+ candidate = cls._canonicalize_option_name(option)
1007
+ if not candidate:
1008
+ continue
1009
+ unsafe_option = canonical_unsafe_options.get(candidate)
973
1010
  if unsafe_option is not None:
974
1011
  raise UnsafeOptionError(f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it.")
1012
+ option_token = option.split("=", 1)[0].split(None, 1)[0]
1013
+ if option_token.startswith("-") and not option_token.startswith("--"):
1014
+ for option_char in option_token[1:]:
1015
+ unsafe_option = unsafe_short_options.get(option_char)
1016
+ if unsafe_option is not None:
1017
+ raise UnsafeOptionError(
1018
+ f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
1019
+ )
1020
+ if option_char not in clusterable_short_options:
1021
+ break
1022
+ if not (option.startswith("--") or (options_are_kwargs and len(candidate) > 1)):
1023
+ continue
1024
+ for canonical, unsafe_option in canonical_unsafe_options.items():
1025
+ if canonical.startswith(candidate):
1026
+ raise UnsafeOptionError(
1027
+ f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
1028
+ )
1029
+
1030
+ @classmethod
1031
+ def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[str, Any]] = None) -> List[str]:
1032
+ """Collect possible option spellings before command-line transformation."""
1033
+ options = [
1034
+ option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-")
1035
+ ]
1036
+ if kwargs:
1037
+ for key, value in kwargs.items():
1038
+ values = value if isinstance(value, (list, tuple)) else (value,)
1039
+ if any(value is True or (value is not False and value is not None) for value in values):
1040
+ key = str(key)
1041
+ options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")
1042
+ return options
975
1043
 
976
1044
  AutoInterrupt: TypeAlias = _AutoInterrupt
977
1045
 
@@ -1030,6 +1098,22 @@ class Git(metaclass=_GitMeta):
1030
1098
 
1031
1099
  self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
1032
1100
 
1101
+ def ls_remote(
1102
+ self,
1103
+ *args: Any,
1104
+ allow_unsafe_options: bool = False,
1105
+ **kwargs: Any,
1106
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
1107
+ """List references in a remote repository.
1108
+
1109
+ :param allow_unsafe_options:
1110
+ Allow unsafe options, like ``--upload-pack``.
1111
+ """
1112
+ if not allow_unsafe_options:
1113
+ candidate_options = self._option_candidates(args, kwargs)
1114
+ Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options)
1115
+ return self._call_process("ls_remote", *args, **kwargs)
1116
+
1033
1117
  @property
1034
1118
  def working_dir(self) -> Union[None, PathLike]:
1035
1119
  """:return: Git directory we are working on"""
@@ -1131,9 +1215,28 @@ class Git(metaclass=_GitMeta):
1131
1215
  information (stdout).
1132
1216
 
1133
1217
  :param command:
1134
- The command argument list to execute.
1135
- It should be a sequence of program arguments, or a string. The
1136
- program to execute is the first item in the args sequence or string.
1218
+ The command to execute. A sequence of program arguments is recommended.
1219
+ A string is also accepted, but its meaning is strongly platform-dependent.
1220
+
1221
+ By default, a shell is not used. On Unix-like systems, a string is the whole
1222
+ program name (so ``"git log -n 1"`` raises :class:`GitCommandNotFound`). On
1223
+ Windows, the program parses the arguments itself, so multi-word strings can
1224
+ work but are not portable.
1225
+
1226
+ Avoid ``shell=True`` (and :attr:`Git.USE_SHELL`): this runs the command in
1227
+ a shell, which is generally unsafe. The shell interprets metacharacters
1228
+ such as ``;``, ``|``, ``&``, ``$(...)``, ``$VAR``, ``%VAR%``, and ``^``
1229
+ (depending on the platform) as syntax. Any untrusted text in the command
1230
+ can then execute arbitrary OS commands. See :attr:`Git.USE_SHELL`.
1231
+
1232
+ Producing a sequence automatically by :func:`shlex.split` and passing it
1233
+ as the command is far safer than ``shell=True``. But :func:`shlex.split`
1234
+ parses POSIX shell syntax on all systems, and the result is still unsafe
1235
+ for anything but *fixed, fully trusted* strings. Do not use it on strings
1236
+ built by interpolating values: whitespace or quoting in an untrusted value
1237
+ can still inject arguments. For input derived in any way from untrusted
1238
+ data, build the argument sequence yourself, while ensuring each argument
1239
+ is fully sanitized.
1137
1240
 
1138
1241
  :param istream:
1139
1242
  Standard input filehandle passed to :class:`subprocess.Popen`.
@@ -1201,6 +1304,11 @@ class Git(metaclass=_GitMeta):
1201
1304
  needed (nor useful) to work around any known operating system specific
1202
1305
  issues.
1203
1306
 
1307
+ On Unix-like systems, when migrating away from passing string commands with
1308
+ ``shell=True``, :func:`shlex.split` may serve as a transitional step in rare
1309
+ cases, with extreme care. (Drop ``shell=True`` and pass the resulting
1310
+ sequence as the command.) See the `command` parameter above on the risks.
1311
+
1204
1312
  :param env:
1205
1313
  A dictionary of environment variables to be passed to
1206
1314
  :class:`subprocess.Popen`.
@@ -1512,7 +1620,7 @@ class Git(metaclass=_GitMeta):
1512
1620
  return args
1513
1621
 
1514
1622
  @classmethod
1515
- def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
1623
+ def _unpack_args(cls, arg_list: Sequence[Any]) -> List[str]:
1516
1624
  outlist = []
1517
1625
  if isinstance(arg_list, (list, tuple)):
1518
1626
  for arg in arg_list:
@@ -1588,7 +1696,7 @@ class Git(metaclass=_GitMeta):
1588
1696
 
1589
1697
  turns into::
1590
1698
 
1591
- git rev-list --max-count=10 --header=master
1699
+ git rev-list --max-count=10 --header master
1592
1700
 
1593
1701
  :return:
1594
1702
  Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
@@ -72,6 +72,9 @@ CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbr
72
72
  See: https://git-scm.com/docs/git-config#_conditional_includes
73
73
  """
74
74
 
75
+ UNSAFE_CONFIG_CHARS_RE = re.compile(r"[\r\n\x00]")
76
+ """Characters that cannot be safely written in config names or values."""
77
+
75
78
 
76
79
  class MetaParserBuilder(abc.ABCMeta): # noqa: B024
77
80
  """Utility class wrapping base-class methods into decorators that assure read-only
@@ -631,6 +634,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
631
634
  files_to_read = list(self._file_or_files)
632
635
  # END ensure we have a copy of the paths to handle
633
636
 
637
+ files_to_read = [osp.abspath(path) if isinstance(path, (str, os.PathLike)) else path for path in files_to_read]
638
+
634
639
  seen = set(files_to_read)
635
640
  num_read_include_files = 0
636
641
  while files_to_read:
@@ -778,6 +783,7 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
778
783
 
779
784
  def add_section(self, section: "cp._SectionName") -> None:
780
785
  """Assures added options will stay in order."""
786
+ self._assure_config_name_safe(section, "section")
781
787
  return super().add_section(section)
782
788
 
783
789
  @property
@@ -884,10 +890,14 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
884
890
 
885
891
  def _value_to_string_safe(self, value: Union[str, bytes, int, float, bool]) -> str:
886
892
  value_str = self._value_to_string(value)
887
- if re.search(r"[\r\n\x00]", value_str):
893
+ if UNSAFE_CONFIG_CHARS_RE.search(value_str):
888
894
  raise ValueError("Git config values must not contain CR, LF, or NUL")
889
895
  return value_str
890
896
 
897
+ def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None:
898
+ if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):
899
+ raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label)
900
+
891
901
  @needs_values
892
902
  @set_dirty_and_flush_changes
893
903
  def set(
@@ -896,6 +906,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
896
906
  option: str,
897
907
  value: Union[str, bytes, int, float, bool, None] = None,
898
908
  ) -> None:
909
+ self._assure_config_name_safe(section, "section")
910
+ self._assure_config_name_safe(option, "option")
899
911
  if value is not None:
900
912
  value = self._value_to_string_safe(value)
901
913
  return super().set(section, option, value)
@@ -920,6 +932,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
920
932
  :return:
921
933
  This instance
922
934
  """
935
+ self._assure_config_name_safe(section, "section")
936
+ self._assure_config_name_safe(option, "option")
923
937
  value_str = self._value_to_string_safe(value)
924
938
  if not self.has_section(section):
925
939
  self.add_section(section)
@@ -948,6 +962,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
948
962
  :return:
949
963
  This instance
950
964
  """
965
+ self._assure_config_name_safe(section, "section")
966
+ self._assure_config_name_safe(option, "option")
951
967
  value_str = self._value_to_string_safe(value)
952
968
  if not self.has_section(section):
953
969
  self.add_section(section)
@@ -968,6 +984,7 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
968
984
  """
969
985
  if not self.has_section(section):
970
986
  raise ValueError("Source section '%s' doesn't exist" % section)
987
+ self._assure_config_name_safe(new_name, "section")
971
988
  if self.has_section(new_name):
972
989
  raise ValueError("Destination section '%s' already exists" % new_name)
973
990
 
@@ -3,7 +3,7 @@
3
3
  # This module is part of GitPython and is released under the
4
4
  # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
5
 
6
- __all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"]
6
+ __all__ = ["DiffConstants", "NULL_TREE", "NULL_TREE_SHA", "INDEX", "Diffable", "DiffIndex", "Diff"]
7
7
 
8
8
  import enum
9
9
  import re
@@ -84,6 +84,9 @@ This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed
84
84
  :const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
85
85
  """
86
86
 
87
+ NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
88
+ """SHA of Git's canonical empty tree object."""
89
+
87
90
  INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
88
91
  """Stand-in indicating you want to diff against the index.
89
92
 
@@ -599,7 +602,14 @@ class Diff:
599
602
 
600
603
  # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
601
604
  text_list: List[bytes] = []
602
- handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)
605
+ stderr_list: List[bytes] = []
606
+
607
+ def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
608
+ finalize_process(proc, stderr=b"".join(stderr_list))
609
+
610
+ handle_process_output(
611
+ proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
612
+ )
603
613
 
604
614
  # For now, we have to bake the stream.
605
615
  text = b"".join(text_list)
@@ -765,11 +775,16 @@ class Diff:
765
775
  # :100644 100644 687099101... 37c5e30c8... M .gitignore
766
776
 
767
777
  index: "DiffIndex" = DiffIndex()
778
+ stderr_list: List[bytes] = []
779
+
780
+ def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
781
+ finalize_process(proc, stderr=b"".join(stderr_list))
782
+
768
783
  handle_process_output(
769
784
  proc,
770
785
  lambda byt: cls._handle_diff_line(byt, repo, index),
771
- None,
772
- finalize_process,
786
+ stderr_list.append,
787
+ finalize_process_with_stderr,
773
788
  decode_streams=False,
774
789
  )
775
790
 
@@ -1480,12 +1480,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1480
1480
 
1481
1481
  return self
1482
1482
 
1483
- # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1484
- # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
1485
1483
  def diff(
1486
1484
  self,
1487
- other: Union[ # type: ignore[override]
1485
+ other: Union[
1488
1486
  Literal[git_diff.DiffConstants.INDEX],
1487
+ Literal[git_diff.DiffConstants.NULL_TREE],
1489
1488
  "Tree",
1490
1489
  "Commit",
1491
1490
  str,
@@ -1512,6 +1511,44 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1512
1511
  if other is self.INDEX:
1513
1512
  return git_diff.DiffIndex()
1514
1513
 
1514
+ if other == git_diff.NULL_TREE or other == git_diff.NULL_TREE_SHA:
1515
+ args: List[Union[PathLike, str]] = [
1516
+ "--cached",
1517
+ git_diff.NULL_TREE_SHA,
1518
+ "--abbrev=40",
1519
+ "--full-index",
1520
+ ]
1521
+
1522
+ if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
1523
+ args.append("-M")
1524
+
1525
+ if create_patch:
1526
+ args.append("-p")
1527
+ args.append("--no-ext-diff")
1528
+ else:
1529
+ args.append("--raw")
1530
+ args.append("-z")
1531
+
1532
+ args.append("--no-color")
1533
+
1534
+ if paths is not None and not isinstance(paths, (tuple, list)):
1535
+ paths = [paths]
1536
+
1537
+ if paths:
1538
+ args.append("--")
1539
+ args.extend(paths)
1540
+
1541
+ kwargs["as_process"] = True
1542
+ proc = self.repo.git.diff(*args, **kwargs)
1543
+
1544
+ diff_method = (
1545
+ git_diff.Diff._index_from_patch_format if create_patch else git_diff.Diff._index_from_raw_format
1546
+ )
1547
+ index = diff_method(self.repo, proc)
1548
+
1549
+ proc.wait()
1550
+ return index
1551
+
1515
1552
  # Index against anything but None is a reverse diff with the respective item.
1516
1553
  # Handle existing -R flags properly.
1517
1554
  # Transform strings to the object so that we can call diff on it.
@@ -86,6 +86,12 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
86
86
  # INVARIANTS
87
87
  default_encoding = "UTF-8"
88
88
 
89
+ # Options to :manpage:`git-rev-list(1)` that can overwrite files.
90
+ unsafe_git_rev_options = [
91
+ "--output",
92
+ "-o",
93
+ ]
94
+
89
95
  type: Literal["commit"] = "commit"
90
96
 
91
97
  __slots__ = (
@@ -302,6 +308,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
302
308
  repo: "Repo",
303
309
  rev: Union[str, "Commit", "SymbolicReference"],
304
310
  paths: Union[PathLike, Sequence[PathLike]] = "",
311
+ allow_unsafe_options: bool = False,
305
312
  **kwargs: Any,
306
313
  ) -> Iterator["Commit"]:
307
314
  R"""Find all commits matching the given criteria.
@@ -330,6 +337,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
330
337
  raise ValueError("--pretty cannot be used as parsing expects single sha's only")
331
338
  # END handle pretty
332
339
 
340
+ if not allow_unsafe_options:
341
+ Git.check_unsafe_options(
342
+ options=Git._option_candidates([rev], kwargs), unsafe_options=cls.unsafe_git_rev_options
343
+ )
344
+
333
345
  # Use -- in all cases, to prevent possibility of ambiguous arguments.
334
346
  # See https://github.com/gitpython-developers/GitPython/issues/264.
335
347
 
@@ -374,6 +386,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
374
386
  """Create a git stat from changes between this commit and its first parent
375
387
  or from all changes done if this is the very first commit.
376
388
 
389
+ :note:
390
+ If this commit is at the boundary of a shallow clone, this will
391
+ raise :exc:`~git.exc.GitCommandError`, since the parent object
392
+ was never fetched and only exists as a reference on this commit.
393
+
377
394
  :return:
378
395
  :class:`Stats`
379
396
  """
@@ -517,6 +517,9 @@ class FetchInfo(IterableObj):
517
517
  raise NotImplementedError
518
518
 
519
519
 
520
+ Progress = Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None]
521
+
522
+
520
523
  class Remote(LazyMixin, IterableObj):
521
524
  """Provides easy read and write access to a git remote.
522
525
 
@@ -872,7 +875,7 @@ class Remote(LazyMixin, IterableObj):
872
875
  def _get_fetch_info_from_stderr(
873
876
  self,
874
877
  proc: "Git.AutoInterrupt",
875
- progress: Union[Callable[..., Any], RemoteProgress, None],
878
+ progress: Progress,
876
879
  kill_after_timeout: Union[None, float] = None,
877
880
  ) -> IterableList["FetchInfo"]:
878
881
  progress = to_progress_instance(progress)
@@ -1000,7 +1003,7 @@ class Remote(LazyMixin, IterableObj):
1000
1003
  def fetch(
1001
1004
  self,
1002
1005
  refspec: Union[str, List[str], None] = None,
1003
- progress: Union[RemoteProgress, None, "UpdateProgress"] = None,
1006
+ progress: Progress = None,
1004
1007
  verbose: bool = True,
1005
1008
  kill_after_timeout: Union[None, float] = None,
1006
1009
  allow_unsafe_protocols: bool = False,
@@ -1068,7 +1071,10 @@ class Remote(LazyMixin, IterableObj):
1068
1071
  Git.check_unsafe_protocols(ref)
1069
1072
 
1070
1073
  if not allow_unsafe_options:
1071
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
1074
+ Git.check_unsafe_options(
1075
+ options=Git._option_candidates([], kwargs),
1076
+ unsafe_options=self.unsafe_git_fetch_options,
1077
+ )
1072
1078
 
1073
1079
  proc = self.repo.git.fetch(
1074
1080
  "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
@@ -1081,7 +1087,7 @@ class Remote(LazyMixin, IterableObj):
1081
1087
  def pull(
1082
1088
  self,
1083
1089
  refspec: Union[str, List[str], None] = None,
1084
- progress: Union[RemoteProgress, "UpdateProgress", None] = None,
1090
+ progress: Progress = None,
1085
1091
  kill_after_timeout: Union[None, float] = None,
1086
1092
  allow_unsafe_protocols: bool = False,
1087
1093
  allow_unsafe_options: bool = False,
@@ -1122,7 +1128,10 @@ class Remote(LazyMixin, IterableObj):
1122
1128
  Git.check_unsafe_protocols(ref)
1123
1129
 
1124
1130
  if not allow_unsafe_options:
1125
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
1131
+ Git.check_unsafe_options(
1132
+ options=Git._option_candidates([], kwargs),
1133
+ unsafe_options=self.unsafe_git_pull_options,
1134
+ )
1126
1135
 
1127
1136
  proc = self.repo.git.pull(
1128
1137
  "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
@@ -1135,7 +1144,7 @@ class Remote(LazyMixin, IterableObj):
1135
1144
  def push(
1136
1145
  self,
1137
1146
  refspec: Union[str, List[str], None] = None,
1138
- progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None,
1147
+ progress: Progress = None,
1139
1148
  kill_after_timeout: Union[None, float] = None,
1140
1149
  allow_unsafe_protocols: bool = False,
1141
1150
  allow_unsafe_options: bool = False,
@@ -1195,7 +1204,10 @@ class Remote(LazyMixin, IterableObj):
1195
1204
  Git.check_unsafe_protocols(ref)
1196
1205
 
1197
1206
  if not allow_unsafe_options:
1198
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)
1207
+ Git.check_unsafe_options(
1208
+ options=Git._option_candidates([], kwargs),
1209
+ unsafe_options=self.unsafe_git_push_options,
1210
+ )
1199
1211
 
1200
1212
  proc = self.repo.git.push(
1201
1213
  "--",
@@ -161,6 +161,20 @@ class Repo:
161
161
  https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
162
162
  """
163
163
 
164
+ unsafe_git_archive_options = [
165
+ # Allows arbitrary command execution through the remote git-upload-archive command.
166
+ "--exec",
167
+ # Writes output to a caller-controlled filesystem path.
168
+ "--output",
169
+ "-o",
170
+ ]
171
+
172
+ unsafe_git_revision_options = [
173
+ # This option allows output to be written to arbitrary files before revision parsing.
174
+ "--output",
175
+ "-o",
176
+ ]
177
+
164
178
  # Invariants
165
179
  config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
166
180
  """Represents the configuration level of a configuration file."""
@@ -242,6 +256,28 @@ class Repo:
242
256
  # It's important to normalize the paths, as submodules will otherwise
243
257
  # initialize their repo instances with paths that depend on path-portions
244
258
  # that will not exist after being removed. It's just cleaner.
259
+ if (
260
+ osp.isfile(osp.join(curpath, "gitdir"))
261
+ and osp.isfile(osp.join(curpath, "commondir"))
262
+ and osp.isfile(osp.join(curpath, "HEAD"))
263
+ ):
264
+ git_dir = curpath
265
+
266
+ if "GIT_WORK_TREE" in os.environ:
267
+ self._working_tree_dir = os.getenv("GIT_WORK_TREE")
268
+ else:
269
+ # Linked worktree administrative directories store the path to the
270
+ # worktree's .git file in their gitdir file (without "gitdir: " prefix).
271
+ with open(osp.join(git_dir, "gitdir")) as fp:
272
+ worktree_gitfile = fp.read().strip()
273
+
274
+ if not osp.isabs(worktree_gitfile):
275
+ worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
276
+
277
+ self._working_tree_dir = osp.dirname(worktree_gitfile)
278
+
279
+ break
280
+
245
281
  if is_git_dir(curpath):
246
282
  git_dir = curpath
247
283
  # from man git-config : core.worktree
@@ -273,7 +309,8 @@ class Repo:
273
309
  sm_gitpath = find_worktree_git_dir(dotgit)
274
310
 
275
311
  if sm_gitpath is not None:
276
- git_dir = expand_path(sm_gitpath, expand_vars)
312
+ # worktrees can use relative paths as of Git 2.48, so we join to curpath
313
+ git_dir = osp.normpath(osp.join(curpath, sm_gitpath))
277
314
  self._working_tree_dir = curpath
278
315
  break
279
316
 
@@ -752,6 +789,7 @@ class Repo:
752
789
  self,
753
790
  rev: Union[str, Commit, "SymbolicReference", None] = None,
754
791
  paths: Union[PathLike, Sequence[PathLike]] = "",
792
+ allow_unsafe_options: bool = False,
755
793
  **kwargs: Any,
756
794
  ) -> Iterator[Commit]:
757
795
  """An iterator of :class:`~git.objects.commit.Commit` objects representing the
@@ -769,6 +807,9 @@ class Repo:
769
807
  Arguments to be passed to :manpage:`git-rev-list(1)`.
770
808
  Common ones are ``max_count`` and ``skip``.
771
809
 
810
+ :param allow_unsafe_options:
811
+ Allow unsafe options in the revision argument, like ``--output``.
812
+
772
813
  :note:
773
814
  To receive only commits between two named revisions, use the
774
815
  ``"revA...revB"`` revision specifier.
@@ -779,7 +820,18 @@ class Repo:
779
820
  if rev is None:
780
821
  rev = self.head.commit
781
822
 
782
- return Commit.iter_items(self, rev, paths, **kwargs)
823
+ if not allow_unsafe_options:
824
+ Git.check_unsafe_options(
825
+ options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options
826
+ )
827
+
828
+ return Commit.iter_items(
829
+ self,
830
+ rev,
831
+ paths,
832
+ allow_unsafe_options=allow_unsafe_options,
833
+ **kwargs,
834
+ )
783
835
 
784
836
  def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
785
837
  R"""Find the closest common ancestor for the given revision
@@ -1056,7 +1108,9 @@ class Repo:
1056
1108
  )
1057
1109
  return active_branch
1058
1110
 
1059
- def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
1111
+ def blame_incremental(
1112
+ self, rev: str | HEAD | None, file: str, allow_unsafe_options: bool = False, **kwargs: Any
1113
+ ) -> Iterator["BlameEntry"]:
1060
1114
  """Iterator for blame information for the given file at the given revision.
1061
1115
 
1062
1116
  Unlike :meth:`blame`, this does not return the actual file's contents, only a
@@ -1067,6 +1121,9 @@ class Repo:
1067
1121
  uncommitted changes. Otherwise, anything successfully parsed by
1068
1122
  :manpage:`git-rev-parse(1)` is a valid option.
1069
1123
 
1124
+ :param allow_unsafe_options:
1125
+ Allow unsafe options in revision argument, like ``--output``.
1126
+
1070
1127
  :return:
1071
1128
  Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
1072
1129
  commit to blame for the line, and range indicates a span of line numbers in
@@ -1075,6 +1132,10 @@ class Repo:
1075
1132
  If you combine all line number ranges outputted by this command, you should get
1076
1133
  a continuous range spanning all line numbers in the file.
1077
1134
  """
1135
+ if not allow_unsafe_options:
1136
+ Git.check_unsafe_options(
1137
+ options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options
1138
+ )
1078
1139
 
1079
1140
  data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
1080
1141
  commits: Dict[bytes, Commit] = {}
@@ -1153,7 +1214,8 @@ class Repo:
1153
1214
  rev: Union[str, HEAD, None],
1154
1215
  file: str,
1155
1216
  incremental: bool = False,
1156
- rev_opts: Optional[List[str]] = None,
1217
+ rev_opts: Optional[Sequence[str]] = None,
1218
+ allow_unsafe_options: bool = False,
1157
1219
  **kwargs: Any,
1158
1220
  ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
1159
1221
  """The blame information for the given file at the given revision.
@@ -1163,6 +1225,9 @@ class Repo:
1163
1225
  uncommitted changes. Otherwise, anything successfully parsed by
1164
1226
  :manpage:`git-rev-parse(1)` is a valid option.
1165
1227
 
1228
+ :param allow_unsafe_options:
1229
+ Allow unsafe options in revision argument, like ``--output``.
1230
+
1166
1231
  :return:
1167
1232
  list: [git.Commit, list: [<line>]]
1168
1233
 
@@ -1172,9 +1237,14 @@ class Repo:
1172
1237
  appearance.
1173
1238
  """
1174
1239
  if incremental:
1175
- return self.blame_incremental(rev, file, **kwargs)
1176
- rev_opts = rev_opts or []
1177
- data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)
1240
+ return self.blame_incremental(rev, file, allow_unsafe_options=allow_unsafe_options, **kwargs)
1241
+ rev_opts_list = list(rev_opts or [])
1242
+ if not allow_unsafe_options:
1243
+ Git.check_unsafe_options(
1244
+ options=Git._option_candidates([rev, rev_opts_list], kwargs),
1245
+ unsafe_options=self.unsafe_git_revision_options,
1246
+ )
1247
+ data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs)
1178
1248
  commits: Dict[str, Commit] = {}
1179
1249
  blames: List[List[Commit | List[str | bytes] | None]] = []
1180
1250
 
@@ -1385,7 +1455,10 @@ class Repo:
1385
1455
  if not allow_unsafe_protocols:
1386
1456
  Git.check_unsafe_protocols(url)
1387
1457
  if not allow_unsafe_options:
1388
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
1458
+ Git.check_unsafe_options(
1459
+ options=Git._option_candidates([], kwargs),
1460
+ unsafe_options=cls.unsafe_git_clone_options,
1461
+ )
1389
1462
  if not allow_unsafe_options and multi:
1390
1463
  Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)
1391
1464
 
@@ -1560,6 +1633,8 @@ class Repo:
1560
1633
  ostream: Union[TextIO, BinaryIO],
1561
1634
  treeish: Optional[str] = None,
1562
1635
  prefix: Optional[str] = None,
1636
+ allow_unsafe_options: bool = False,
1637
+ allow_unsafe_protocols: bool = False,
1563
1638
  **kwargs: Any,
1564
1639
  ) -> Repo:
1565
1640
  """Archive the tree at the given revision.
@@ -1582,6 +1657,12 @@ class Repo:
1582
1657
  repository-relative path to a directory or file to place into the archive,
1583
1658
  or a list or tuple of multiple paths.
1584
1659
 
1660
+ :param allow_unsafe_options:
1661
+ Allow unsafe options, like ``--exec`` or ``--output``.
1662
+
1663
+ :param allow_unsafe_protocols:
1664
+ Allow unsafe protocols to be used in ``remote``, like ``ext``.
1665
+
1585
1666
  :raise git.exc.GitCommandError:
1586
1667
  If something went wrong.
1587
1668
 
@@ -1592,6 +1673,14 @@ class Repo:
1592
1673
  treeish = self.head.commit
1593
1674
  if prefix and "prefix" not in kwargs:
1594
1675
  kwargs["prefix"] = prefix
1676
+ remote = kwargs.get("remote")
1677
+ if not allow_unsafe_protocols and remote is not None:
1678
+ Git.check_unsafe_protocols(str(remote))
1679
+ if not allow_unsafe_options:
1680
+ Git.check_unsafe_options(
1681
+ options=Git._option_candidates([], kwargs),
1682
+ unsafe_options=self.unsafe_git_archive_options,
1683
+ )
1595
1684
  kwargs["output_stream"] = ostream
1596
1685
  path = kwargs.pop("path", [])
1597
1686
  path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
@@ -1,15 +0,0 @@
1
- # How to contribute
2
-
3
- The following is a short step-by-step rundown of what one typically would do to contribute.
4
-
5
- - [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub.
6
- - For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`.
7
- - Please try to **write a test that fails unless the contribution is present.**
8
- - Try to avoid massive commits and prefer to take small steps, with one commit for each.
9
- - Feel free to add yourself to AUTHORS file.
10
- - Create a pull request.
11
-
12
- ## Fuzzing Test Specific Documentation
13
-
14
- For details related to contributing to the fuzzing test suite and OSS-Fuzz integration, please
15
- refer to the dedicated [fuzzing README](./fuzzing/README.md).
gitpython-3.1.49/VERSION DELETED
@@ -1 +0,0 @@
1
- 3.1.49
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes