GitPython 3.1.57__tar.gz → 3.1.59__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 (69) hide show
  1. {gitpython-3.1.57 → gitpython-3.1.59/GitPython.egg-info}/PKG-INFO +1 -1
  2. {gitpython-3.1.57/GitPython.egg-info → gitpython-3.1.59}/PKG-INFO +1 -1
  3. gitpython-3.1.59/VERSION +1 -0
  4. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/changes.rst +37 -2
  5. {gitpython-3.1.57 → gitpython-3.1.59}/git/__init__.py +1 -1
  6. {gitpython-3.1.57 → gitpython-3.1.59}/git/cmd.py +23 -6
  7. {gitpython-3.1.57 → gitpython-3.1.59}/git/config.py +17 -5
  8. {gitpython-3.1.57 → gitpython-3.1.59}/git/db.py +24 -1
  9. {gitpython-3.1.57 → gitpython-3.1.59}/git/diff.py +4 -3
  10. {gitpython-3.1.57 → gitpython-3.1.59}/git/index/base.py +64 -17
  11. {gitpython-3.1.57 → gitpython-3.1.59}/git/index/fun.py +100 -2
  12. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/commit.py +5 -2
  13. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/submodule/base.py +43 -19
  14. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/submodule/root.py +3 -2
  15. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/head.py +26 -1
  16. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/log.py +1 -0
  17. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/symbolic.py +1 -1
  18. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/tag.py +6 -4
  19. {gitpython-3.1.57 → gitpython-3.1.59}/git/repo/base.py +53 -12
  20. {gitpython-3.1.57 → gitpython-3.1.59}/git/util.py +62 -0
  21. {gitpython-3.1.57 → gitpython-3.1.59}/pyproject.toml +2 -2
  22. gitpython-3.1.57/VERSION +0 -1
  23. {gitpython-3.1.57 → gitpython-3.1.59}/AUTHORS +0 -0
  24. {gitpython-3.1.57 → gitpython-3.1.59}/CHANGES +0 -0
  25. {gitpython-3.1.57 → gitpython-3.1.59}/CONTRIBUTING.md +0 -0
  26. {gitpython-3.1.57 → gitpython-3.1.59}/GitPython.egg-info/SOURCES.txt +0 -0
  27. {gitpython-3.1.57 → gitpython-3.1.59}/GitPython.egg-info/dependency_links.txt +0 -0
  28. {gitpython-3.1.57 → gitpython-3.1.59}/GitPython.egg-info/not-zip-safe +0 -0
  29. {gitpython-3.1.57 → gitpython-3.1.59}/GitPython.egg-info/requires.txt +0 -0
  30. {gitpython-3.1.57 → gitpython-3.1.59}/GitPython.egg-info/top_level.txt +0 -0
  31. {gitpython-3.1.57 → gitpython-3.1.59}/LICENSE +0 -0
  32. {gitpython-3.1.57 → gitpython-3.1.59}/MANIFEST.in +0 -0
  33. {gitpython-3.1.57 → gitpython-3.1.59}/README.md +0 -0
  34. {gitpython-3.1.57 → gitpython-3.1.59}/doc/.gitignore +0 -0
  35. {gitpython-3.1.57 → gitpython-3.1.59}/doc/Makefile +0 -0
  36. {gitpython-3.1.57 → gitpython-3.1.59}/doc/requirements.txt +0 -0
  37. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/conf.py +0 -0
  38. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/index.rst +0 -0
  39. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/intro.rst +0 -0
  40. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/quickstart.rst +0 -0
  41. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/reference.rst +0 -0
  42. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/roadmap.rst +0 -0
  43. {gitpython-3.1.57 → gitpython-3.1.59}/doc/source/tutorial.rst +0 -0
  44. {gitpython-3.1.57 → gitpython-3.1.59}/git/compat.py +0 -0
  45. {gitpython-3.1.57 → gitpython-3.1.59}/git/exc.py +0 -0
  46. {gitpython-3.1.57 → gitpython-3.1.59}/git/index/__init__.py +0 -0
  47. {gitpython-3.1.57 → gitpython-3.1.59}/git/index/typ.py +0 -0
  48. {gitpython-3.1.57 → gitpython-3.1.59}/git/index/util.py +0 -0
  49. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/__init__.py +0 -0
  50. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/base.py +0 -0
  51. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/blob.py +0 -0
  52. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/fun.py +0 -0
  53. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/submodule/__init__.py +0 -0
  54. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/submodule/util.py +0 -0
  55. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/tag.py +0 -0
  56. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/tree.py +0 -0
  57. {gitpython-3.1.57 → gitpython-3.1.59}/git/objects/util.py +0 -0
  58. {gitpython-3.1.57 → gitpython-3.1.59}/git/py.typed +0 -0
  59. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/__init__.py +0 -0
  60. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/reference.py +0 -0
  61. {gitpython-3.1.57 → gitpython-3.1.59}/git/refs/remote.py +0 -0
  62. {gitpython-3.1.57 → gitpython-3.1.59}/git/remote.py +0 -0
  63. {gitpython-3.1.57 → gitpython-3.1.59}/git/repo/__init__.py +0 -0
  64. {gitpython-3.1.57 → gitpython-3.1.59}/git/repo/fun.py +0 -0
  65. {gitpython-3.1.57 → gitpython-3.1.59}/git/types.py +0 -0
  66. {gitpython-3.1.57 → gitpython-3.1.59}/requirements.txt +0 -0
  67. {gitpython-3.1.57 → gitpython-3.1.59}/setup.cfg +0 -0
  68. {gitpython-3.1.57 → gitpython-3.1.59}/setup.py +0 -0
  69. {gitpython-3.1.57 → gitpython-3.1.59}/test-requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.57
3
+ Version: 3.1.59
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.57
3
+ Version: 3.1.59
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.59
@@ -2,6 +2,41 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 3.1.59
6
+ ======
7
+
8
+ Security fixes for
9
+
10
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-5xxx-qhh7-9287
11
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3wxw-xv34-2frg
12
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-8mcc-hrx5-hvxc
13
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-284h-m62q-gf8w
14
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-7833-fr7j-v32q
15
+
16
+ If you can, also try and provide feedback on the upcoming v4 branch
17
+ https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
18
+
19
+ See the following for all changes.
20
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.59
21
+
22
+ 3.1.58
23
+ ======
24
+
25
+ Security fixes for
26
+
27
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hh9p-6wh2-4mfc
28
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-9rj7-rf2p-w77r
29
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-4gmw-gg2m-w46p
30
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-wvpp-8hx9-p66j
31
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-jm78-9fvv-mhgr
32
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hmq2-w58f-27jc
33
+
34
+ If you can, also try and provide feedback on the upcoming v4 branch
35
+ https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
36
+
37
+ See the following for all changes.
38
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.58
39
+
5
40
  3.1.57
6
41
  ======
7
42
 
@@ -14,7 +49,7 @@ If you can, also try and provide feedback on the upcoming v4 branch
14
49
  https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
15
50
 
16
51
  See the following for all changes.
17
- https://github.com/gitpython-developers/GitPython/releases/tag/3.1.55
52
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.57
18
53
 
19
54
  3.1.56
20
55
  ======
@@ -27,7 +62,7 @@ If you can, also try and provide feedback on the upcoming v4 branch
27
62
  https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
28
63
 
29
64
  See the following for all changes.
30
- https://github.com/gitpython-developers/GitPython/releases/tag/3.1.55
65
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.56
31
66
 
32
67
  3.1.55
33
68
  ======
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.57'
89
+ __version__ = '3.1.59'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
@@ -652,6 +652,13 @@ class Git(metaclass=_GitMeta):
652
652
  unsafe_git_ls_remote_options = [
653
653
  # This option allows arbitrary command execution in git-ls-remote.
654
654
  "--upload-pack",
655
+ "--exec",
656
+ ]
657
+
658
+ unsafe_git_pathspec_from_file_options = [
659
+ # Reads pathspecs from a caller-controlled file. Some commands include an
660
+ # unmatched pathspec in their error output, which can disclose the file.
661
+ "--pathspec-from-file",
655
662
  ]
656
663
 
657
664
  def __getstate__(self) -> Dict[str, Any]:
@@ -970,7 +977,9 @@ class Git(metaclass=_GitMeta):
970
977
  return dashify(option_tokens[0])
971
978
 
972
979
  @classmethod
973
- def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
980
+ def check_unsafe_options(
981
+ cls, options: List[str], unsafe_options: List[str], clusterable_short_options: str = "46flnqsv"
982
+ ) -> None:
974
983
  """Raise :class:`~git.exc.UnsafeOptionError` for blocked option spellings.
975
984
 
976
985
  In addition to exact matches, this rejects abbreviated long options accepted
@@ -1005,7 +1014,7 @@ class Git(metaclass=_GitMeta):
1005
1014
  # These value-less Git flags can be clustered before another short option
1006
1015
  # (for example, ``-fuVALUE``). Stop at any other character because it may
1007
1016
  # begin an attached value, as ``o`` does in the safe option ``-oupstream``.
1008
- clusterable_short_options = frozenset("46flnqsv")
1017
+ clusterable_short_options_set = frozenset(clusterable_short_options)
1009
1018
  options_are_kwargs = all(not option.startswith("-") for option in options)
1010
1019
  for option in options:
1011
1020
  candidate = cls._canonicalize_option_name(option)
@@ -1022,7 +1031,7 @@ class Git(metaclass=_GitMeta):
1022
1031
  raise UnsafeOptionError(
1023
1032
  f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
1024
1033
  )
1025
- if option_char not in clusterable_short_options:
1034
+ if option_char not in clusterable_short_options_set:
1026
1035
  break
1027
1036
  if not (option.startswith("--") or (options_are_kwargs and len(candidate) > 1)):
1028
1037
  continue
@@ -1044,13 +1053,21 @@ class Git(metaclass=_GitMeta):
1044
1053
  values = value if isinstance(value, (list, tuple)) else (value,)
1045
1054
  if any(value is True or (value is not False and value is not None) for value in values):
1046
1055
  key = str(key)
1047
- options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")
1048
- if len(key) == 1 and split_single_char_options:
1056
+ if len(key) != 1:
1057
+ options.append(f"--{dashify(key)}")
1058
+ elif split_single_char_options:
1059
+ options.append(f"-{key}")
1049
1060
  options.extend(
1050
1061
  str(value)
1051
1062
  for value in values
1052
1063
  if value is not True and value not in (False, None) and str(value).startswith("-")
1053
1064
  )
1065
+ else:
1066
+ options.extend(
1067
+ f"-{key}" if value is True else f"-{key}{value}"
1068
+ for value in values
1069
+ if value is True or (value is not False and value is not None)
1070
+ )
1054
1071
  return options
1055
1072
 
1056
1073
  AutoInterrupt: TypeAlias = _AutoInterrupt
@@ -1119,7 +1136,7 @@ class Git(metaclass=_GitMeta):
1119
1136
  """List references in a remote repository.
1120
1137
 
1121
1138
  :param allow_unsafe_options:
1122
- Allow unsafe options, like ``--upload-pack``.
1139
+ Allow unsafe options, like ``--upload-pack`` or ``--exec``.
1123
1140
  """
1124
1141
  if not allow_unsafe_options:
1125
1142
  candidate_options = self._option_candidates(args, kwargs)
@@ -75,6 +75,9 @@ See: https://git-scm.com/docs/git-config#_conditional_includes
75
75
  UNSAFE_CONFIG_CHARS_RE = re.compile(r"[\r\n\x00]")
76
76
  """Characters that cannot be safely written in config names or values."""
77
77
 
78
+ VALID_CONFIG_OPTION_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
79
+ """Pattern for option names that can be written without changing config syntax."""
80
+
78
81
 
79
82
  class MetaParserBuilder(abc.ABCMeta): # noqa: B024
80
83
  """Utility class wrapping base-class methods into decorators that assure read-only
@@ -577,8 +580,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
577
580
 
578
581
  if keyword in ["gitdir", "gitdir/i"]:
579
582
  value = osp.expanduser(value)
583
+ git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None
584
+ if sys.platform == "win32":
585
+ git_dir = git_dir.replace("\\", "/") if git_dir else None
580
586
 
581
- if not any(value.startswith(s) for s in ["./", "/"]):
587
+ drive, _tail = osp.splitdrive(value)
588
+ if not drive and not any(value.startswith(s) for s in ["./", "/"]):
582
589
  value = "**/" + value
583
590
  if value.endswith("/"):
584
591
  value += "**"
@@ -590,9 +597,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
590
597
  lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]",
591
598
  value,
592
599
  )
593
- if self._repo.git_dir:
594
- if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
595
- paths += _all_items(section)
600
+ if git_dir and fnmatch.fnmatchcase(git_dir, value):
601
+ paths += _all_items(section)
596
602
 
597
603
  elif keyword == "onbranch":
598
604
  try:
@@ -699,7 +705,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
699
705
  continue
700
706
 
701
707
  for v in values:
702
- fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
708
+ value = self._value_to_string(v)
709
+ if any(char in value for char in '\n\t\b\\"'):
710
+ value = value.replace("\\", "\\\\").replace('"', '\\"')
711
+ value = '"%s\\\n"' % value.replace("\n", "\\n").replace("\t", "\\t").replace("\b", "\\b")
712
+ fp.write(("\t%s = %s\n" % (key, value)).encode(defenc))
703
713
  # END if key is not __name__
704
714
 
705
715
  # END section writing
@@ -897,6 +907,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
897
907
  def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None:
898
908
  if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):
899
909
  raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label)
910
+ if label == "option" and isinstance(name, str) and not VALID_CONFIG_OPTION_NAME_RE.fullmatch(name):
911
+ raise ValueError("Git config option names may contain only letters, digits, '-', '_', or '.'")
900
912
  if label == "section" and isinstance(name, str):
901
913
  in_quotes = False
902
914
  escaped = False
@@ -5,10 +5,14 @@
5
5
 
6
6
  __all__ = ["GitCmdObjectDB", "GitDB"]
7
7
 
8
- from gitdb.base import OInfo, OStream
8
+ from subprocess import PIPE
9
+
10
+ from gitdb.base import IStream, OInfo, OStream
9
11
  from gitdb.db import GitDB, LooseObjectDB
10
12
  from gitdb.exc import BadObject
13
+ from gitdb.fun import stream_copy
11
14
 
15
+ from git.compat import force_text
12
16
  from git.util import bin_to_hex, hex_to_bin
13
17
  from git.exc import GitCommandError
14
18
 
@@ -46,6 +50,25 @@ class GitCmdObjectDB(LooseObjectDB):
46
50
  hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha))
47
51
  return OStream(hex_to_bin(hexsha), typename, size, stream)
48
52
 
53
+ def store(self, istream: IStream) -> IStream:
54
+ """Store an object using git itself."""
55
+ if istream.binsha is not None or self.ostream() is not None:
56
+ return super().store(istream)
57
+
58
+ proc = self._git.hash_object(
59
+ "-t", force_text(istream.type), "-w", "--stdin", "--literally", as_process=True, istream=PIPE
60
+ )
61
+ assert proc.stdin is not None
62
+ try:
63
+ stream_copy(istream.read, proc.stdin.write, istream.size, self.stream_chunk_size)
64
+ finally:
65
+ proc.stdin.close()
66
+ assert proc.stdout is not None
67
+ hexsha = proc.stdout.read().strip()
68
+ proc.wait()
69
+ istream.binsha = hex_to_bin(hexsha)
70
+ return istream
71
+
49
72
  # { Interface
50
73
 
51
74
  def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
@@ -220,8 +220,8 @@ class Diffable:
220
220
  to be read and diffed.
221
221
 
222
222
  :param allow_unsafe_options:
223
- If ``True``, allow options such as ``--output`` that can write to arbitrary
224
- filesystem paths.
223
+ If ``True``, allow options such as ``--output`` and ``-O`` that can write to
224
+ or read from arbitrary filesystem paths.
225
225
 
226
226
  :param kwargs:
227
227
  Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
@@ -238,7 +238,8 @@ class Diffable:
238
238
  if not allow_unsafe_options:
239
239
  Git.check_unsafe_options(
240
240
  options=Git._option_candidates([other], kwargs),
241
- unsafe_options=self.repo.unsafe_git_revision_options,
241
+ unsafe_options=self.repo.unsafe_git_diff_options,
242
+ clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
242
243
  )
243
244
 
244
245
  args: List[Union[PathLike, Diffable]] = []
@@ -34,6 +34,8 @@ from git.util import (
34
34
  LockedFD,
35
35
  join_path_native,
36
36
  file_contents_ro,
37
+ _is_path_rooted,
38
+ _to_relative_path,
37
39
  to_native_path_linux,
38
40
  unbare_repo,
39
41
  to_bin_sha,
@@ -58,6 +60,7 @@ from typing import (
58
60
  Any,
59
61
  BinaryIO,
60
62
  Callable,
63
+ cast,
61
64
  Dict,
62
65
  Generator,
63
66
  IO,
@@ -131,6 +134,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
131
134
  """
132
135
 
133
136
  unsafe_git_checkout_index_options = ["--prefix"]
137
+ unsafe_git_read_tree_options = ["--index-output"]
134
138
 
135
139
  __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
136
140
 
@@ -256,7 +260,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
256
260
 
257
261
  @post_clear_cache
258
262
  @default_index
259
- def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
263
+ def merge_tree(
264
+ self,
265
+ rhs: Treeish,
266
+ base: Union[None, Treeish] = None,
267
+ allow_unsafe_options: bool = False,
268
+ ) -> "IndexFile":
260
269
  """Merge the given `rhs` treeish into the current index, possibly taking
261
270
  a common base treeish into account.
262
271
 
@@ -270,6 +279,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
270
279
  Optional treeish reference pointing to the common base of `rhs` and this
271
280
  index which equals lhs.
272
281
 
282
+ :param allow_unsafe_options:
283
+ Allow options that may write to arbitrary paths.
284
+
273
285
  :return:
274
286
  self (containing the merge and possibly unmerged entries in case of
275
287
  conflicts)
@@ -280,6 +292,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
280
292
  yourself, you have to commit the changed index (or make a valid tree from
281
293
  it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
282
294
  """
295
+ if not allow_unsafe_options:
296
+ Git.check_unsafe_options(
297
+ options=Git._option_candidates([base, rhs]),
298
+ unsafe_options=self.unsafe_git_read_tree_options,
299
+ )
300
+
283
301
  # -i : ignore working tree status
284
302
  # --aggressive : handle more merge cases
285
303
  # -m : do an actual merge
@@ -324,7 +342,13 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
324
342
  return inst
325
343
 
326
344
  @classmethod
327
- def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
345
+ def from_tree(
346
+ cls,
347
+ repo: "Repo",
348
+ *treeish: Treeish,
349
+ allow_unsafe_options: bool = False,
350
+ **kwargs: Any,
351
+ ) -> "IndexFile":
328
352
  R"""Merge the given treeish revisions into a new index which is returned.
329
353
  The original index will remain unaltered.
330
354
 
@@ -348,6 +372,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
348
372
  :param kwargs:
349
373
  Additional arguments passed to :manpage:`git-read-tree(1)`.
350
374
 
375
+ :param allow_unsafe_options:
376
+ Allow options that may write to arbitrary paths.
377
+
351
378
  :return:
352
379
  New :class:`IndexFile` instance. It will point to a temporary index location
353
380
  which does not exist anymore. If you intend to write such a merged Index,
@@ -365,6 +392,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
365
392
  if len(treeish) == 0 or len(treeish) > 3:
366
393
  raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
367
394
 
395
+ if not allow_unsafe_options:
396
+ Git.check_unsafe_options(
397
+ options=Git._option_candidates(treeish, kwargs),
398
+ unsafe_options=cls.unsafe_git_read_tree_options,
399
+ )
400
+
368
401
  arg_list: List[Union[Treeish, str]] = []
369
402
  # Ignore that the working tree and index possibly are out of date.
370
403
  if len(treeish) > 1:
@@ -655,16 +688,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
655
688
 
656
689
  :raise ValueError:
657
690
  """
658
- if not osp.isabs(path):
659
- return path
660
691
  if self.repo.bare:
661
- raise InvalidGitRepositoryError("require non-bare repository")
662
- if not osp.normpath(path).startswith(str(self.repo.working_tree_dir)):
663
- raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
664
- result = os.path.relpath(path, self.repo.working_tree_dir)
665
- if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep):
666
- result += os.sep
667
- return result
692
+ drive, _tail = osp.splitdrive(os.fspath(path))
693
+ if drive or _is_path_rooted(path):
694
+ raise InvalidGitRepositoryError("paths with a drive or root require a non-bare repository")
695
+ return path
696
+ return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path)
668
697
 
669
698
  def _preprocess_add_items(
670
699
  self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
@@ -731,14 +760,17 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
731
760
  ) -> List[BaseIndexEntry]:
732
761
  entries_added: List[BaseIndexEntry] = []
733
762
  if path_rewriter:
763
+ working_tree_dir = self.repo.working_tree_dir
764
+ if working_tree_dir is None:
765
+ raise InvalidGitRepositoryError("Cannot rewrite paths without a working tree")
766
+ working_tree_dir = str(working_tree_dir)
734
767
  for path in paths:
735
768
  if osp.isabs(path):
736
769
  abspath = path
737
- gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
770
+ gitrelative_path = path[len(working_tree_dir) + 1 :]
738
771
  else:
739
772
  gitrelative_path = path
740
- if self.repo.working_tree_dir:
741
- abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
773
+ abspath = osp.join(working_tree_dir, gitrelative_path)
742
774
  # END obtain relative and absolute paths
743
775
 
744
776
  blob = Blob(
@@ -992,6 +1024,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
992
1024
  self,
993
1025
  items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
994
1026
  working_tree: bool = False,
1027
+ allow_unsafe_options: bool = False,
995
1028
  **kwargs: Any,
996
1029
  ) -> List[str]:
997
1030
  R"""Remove the given items from the index and optionally from the working tree
@@ -1022,6 +1055,10 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1022
1055
  physically removing the respective file. This may fail if there are
1023
1056
  uncommitted changes in it.
1024
1057
 
1058
+ :param allow_unsafe_options:
1059
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
1060
+ :manpage:`git-rm(1)`.
1061
+
1025
1062
  :param kwargs:
1026
1063
  Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1027
1064
  ``r`` to allow recursive removal.
@@ -1033,6 +1070,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1033
1070
  This is interesting to know in case you have provided a directory or globs.
1034
1071
  Paths are relative to the repository.
1035
1072
  """
1073
+ if not allow_unsafe_options:
1074
+ Git.check_unsafe_options(
1075
+ options=Git._option_candidates([], kwargs),
1076
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
1077
+ )
1036
1078
  args = []
1037
1079
  if not working_tree:
1038
1080
  args.append("--cached")
@@ -1414,6 +1456,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1414
1456
  working_tree: bool = False,
1415
1457
  paths: Union[None, Iterable[PathLike]] = None,
1416
1458
  head: bool = False,
1459
+ allow_unsafe_options: bool = False,
1417
1460
  **kwargs: Any,
1418
1461
  ) -> "IndexFile":
1419
1462
  """Reset the index to reflect the tree at the given commit. This will not adjust
@@ -1445,6 +1488,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1445
1488
  The paths need to exist at the commit, otherwise an exception will be
1446
1489
  raised.
1447
1490
 
1491
+ :param allow_unsafe_options:
1492
+ Allow options that may write to arbitrary paths.
1493
+
1448
1494
  :param kwargs:
1449
1495
  Additional keyword arguments passed to :manpage:`git-reset(1)`.
1450
1496
 
@@ -1461,15 +1507,15 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1461
1507
  """
1462
1508
  # What we actually want to do is to merge the tree into our existing index,
1463
1509
  # which is what git-read-tree does.
1464
- new_inst = type(self).from_tree(self.repo, commit)
1510
+ new_inst = type(self).from_tree(self.repo, commit, allow_unsafe_options=allow_unsafe_options)
1465
1511
  if not paths:
1466
1512
  self.entries = new_inst.entries
1467
1513
  else:
1468
1514
  nie = new_inst.entries
1469
1515
  for path in paths:
1470
1516
  path = self._to_relative_path(path)
1517
+ key = entry_key(path, 0)
1471
1518
  try:
1472
- key = entry_key(path, 0)
1473
1519
  self.entries[key] = nie[key]
1474
1520
  except KeyError:
1475
1521
  # If key is not in theirs, it mustn't be in ours.
@@ -1521,7 +1567,8 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1521
1567
  if not allow_unsafe_options:
1522
1568
  Git.check_unsafe_options(
1523
1569
  options=Git._option_candidates([other], kwargs),
1524
- unsafe_options=self.repo.unsafe_git_revision_options,
1570
+ unsafe_options=self.repo.unsafe_git_diff_options,
1571
+ clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
1525
1572
  )
1526
1573
 
1527
1574
  # Only run if we are the default repository index.
@@ -26,7 +26,7 @@ import sys
26
26
  from gitdb.base import IStream
27
27
  from gitdb.typ import str_tree_type
28
28
 
29
- from git.cmd import handle_process_output, safer_popen
29
+ from git.cmd import Git, handle_process_output, safer_popen
30
30
  from git.compat import defenc, force_bytes, force_text, safe_decode
31
31
  from git.exc import HookExecutionError, UnmergedEntriesError
32
32
  from git.objects.fun import (
@@ -79,6 +79,99 @@ def _has_file_extension(path: str) -> str:
79
79
  return osp.splitext(path)[1]
80
80
 
81
81
 
82
+ def _is_in_windows_system_root(path: str) -> bool:
83
+ """Return whether ``path`` is inside the Windows installation directory."""
84
+ system_root = os.environ.get("SystemRoot")
85
+ if not system_root:
86
+ return False
87
+
88
+ system_root = osp.normcase(osp.realpath(system_root))
89
+ path = osp.normcase(osp.realpath(path))
90
+ try:
91
+ return osp.commonpath((system_root, path)) == system_root
92
+ except ValueError:
93
+ # Paths on different drives have no common path on Windows.
94
+ return False
95
+
96
+
97
+ def _which_from_path(command: str) -> Union[str, None]:
98
+ """Resolve ``command`` from PATH, excluding the Windows installation."""
99
+ for directory in os.get_exec_path():
100
+ # Unlike POSIX, Windows does not define an empty PATH entry as the current
101
+ # directory. Skip it rather than letting abspath() turn it into one.
102
+ if not directory:
103
+ continue
104
+ directory = osp.abspath(directory)
105
+ candidate = osp.join(directory, command)
106
+ # SystemRoot contains the WSL launcher stubs. They are valid executables but
107
+ # not suitable for running a Windows Git hook: the hook path and environment
108
+ # were prepared for Git for Windows, and WSL may have no distribution at all.
109
+ if _is_in_windows_system_root(candidate):
110
+ continue
111
+ if osp.isfile(candidate) and os.access(candidate, os.X_OK):
112
+ return candidate
113
+ return None
114
+
115
+
116
+ _GIT_FOR_WINDOWS_PREFIXES = ("mingw64", "mingw32", "clangarm64", "clang64", "clang32", "ucrt64")
117
+
118
+
119
+ def _git_for_windows_root() -> Union[str, None]:
120
+ """Infer a standard Git for Windows root from GitPython's selected executable."""
121
+ git_executable = os.fspath(Git.GIT_PYTHON_GIT_EXECUTABLE or Git.git_exec_name)
122
+ if osp.dirname(git_executable):
123
+ # CreateProcess resolves a relative executable path containing a directory
124
+ # from the parent process cwd, even when Popen supplies a different child cwd.
125
+ git_executable = osp.abspath(git_executable)
126
+ else:
127
+ # GitPython deliberately retains a bare executable name so later PATH changes
128
+ # affect Git commands. Resolve it with the same PATH snapshot used for Bash.
129
+ names = (git_executable,) if _has_file_extension(git_executable) else (git_executable, f"{git_executable}.exe")
130
+ for name in names:
131
+ resolved = _which_from_path(name)
132
+ if resolved is not None:
133
+ git_executable = resolved
134
+ break
135
+ else:
136
+ git_executable = ""
137
+ if not git_executable:
138
+ return None
139
+ if osp.basename(git_executable).lower() not in ("git", "git.exe"):
140
+ return None
141
+
142
+ executable_dir = osp.dirname(git_executable)
143
+ directory_name = osp.basename(executable_dir).lower()
144
+ if directory_name == "cmd":
145
+ # The normal system-wide PATH entry is <git-root>/cmd.
146
+ return osp.dirname(executable_dir)
147
+ if directory_name == "bin":
148
+ prefix = osp.dirname(executable_dir)
149
+ if osp.basename(prefix).lower() in _GIT_FOR_WINDOWS_PREFIXES:
150
+ # Git Bash commonly exposes <git-root>/<platform>/bin/git.exe.
151
+ return osp.dirname(prefix)
152
+ if osp.basename(prefix).lower() != "usr":
153
+ # An explicitly configured Git may be the root-level bin/git.exe. Do
154
+ # not make the same inference from usr/bin: unlike the recognized
155
+ # platform prefixes, "usr" has no reliably bounded parent layout.
156
+ return prefix
157
+ return None
158
+
159
+
160
+ def _git_for_windows_bash() -> Union[str, None]:
161
+ """Return Bash from the Git for Windows installation selected by GitPython."""
162
+ git_root = _git_for_windows_root()
163
+ if git_root is None:
164
+ return None
165
+
166
+ # Match gix-path's precedence: prefer the lightweight bin shim, then the
167
+ # underlying usr/bin executable. Both belong to the same installation as Git.
168
+ for relative_path in ("bin/bash.exe", "usr/bin/bash.exe"):
169
+ candidate = osp.join(git_root, *relative_path.split("/"))
170
+ if osp.isfile(candidate) and os.access(candidate, os.X_OK):
171
+ return candidate
172
+ return None
173
+
174
+
82
175
  def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
83
176
  """Run the commit hook of the given name. Silently ignore hooks that do not exist.
84
177
 
@@ -112,7 +205,12 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
112
205
  # an absolute path in this form, although a relative path is preferable
113
206
  # because it also works with the Windows Subsystem for Linux wrapper.
114
207
  bash_hp = hp
115
- cmd = ["bash.exe", Path(bash_hp).as_posix()]
208
+ # Prefer Bash associated with GitPython's selected Git installation. If
209
+ # that layout is not recognized, use an explicitly configured non-system
210
+ # PATH entry. Preserve the bare fallback for installations that previously
211
+ # relied on WSL or another CreateProcess-resolved Bash.
212
+ bash_executable = _git_for_windows_bash() or _which_from_path("bash.exe") or "bash.exe"
213
+ cmd = [bash_executable, Path(bash_hp).as_posix()]
116
214
 
117
215
  process = safer_popen(
118
216
  cmd + list(args),
@@ -575,11 +575,14 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
575
575
 
576
576
  if hasattr(proc_or_stream, "wait"):
577
577
  proc_or_stream = cast(Popen, proc_or_stream)
578
- if proc_or_stream.stdout is not None:
579
- stream = proc_or_stream.stdout
578
+ stream = proc_or_stream.stdout
579
+ if stream is None:
580
+ raise ValueError("Process has no stdout stream")
580
581
  elif hasattr(proc_or_stream, "readline"):
581
582
  proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast]
582
583
  stream = proc_or_stream
584
+ else:
585
+ raise TypeError("Expected a process or stream")
583
586
 
584
587
  readline = stream.readline
585
588
  while True: