GitPython 3.1.57__py3-none-any.whl → 3.1.59__py3-none-any.whl

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.
git/__init__.py CHANGED
@@ -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
 
git/cmd.py CHANGED
@@ -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)
git/config.py CHANGED
@@ -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
git/db.py CHANGED
@@ -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:
git/diff.py CHANGED
@@ -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]] = []
git/index/base.py CHANGED
@@ -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.
git/index/fun.py CHANGED
@@ -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),
git/objects/commit.py CHANGED
@@ -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:
@@ -6,8 +6,10 @@ __all__ = ["Submodule", "UpdateProgress"]
6
6
  import gc
7
7
  from io import BytesIO
8
8
  import logging
9
+ import ntpath
9
10
  import os
10
11
  import os.path as osp
12
+ import shlex
11
13
  import stat
12
14
  import sys
13
15
  import uuid
@@ -28,6 +30,7 @@ from git.objects.util import TraversableIterableObj
28
30
  from git.util import (
29
31
  IterableList,
30
32
  RemoteProgress,
33
+ _to_relative_path,
31
34
  join_path_native,
32
35
  rmtree,
33
36
  to_native_path_linux,
@@ -268,7 +271,7 @@ class Submodule(IndexObject, TraversableIterableObj):
268
271
  raise ValueError("Cannot write blobs of 'historical' submodule configurations")
269
272
  # END handle writes of historical submodules
270
273
 
271
- return SubmoduleConfigParser(fp_module, read_only=read_only)
274
+ return SubmoduleConfigParser(fp_module, read_only=read_only, merge_includes=False)
272
275
 
273
276
  def _clear_cache(self) -> None:
274
277
  """Clear the possibly changed values."""
@@ -302,8 +305,20 @@ class Submodule(IndexObject, TraversableIterableObj):
302
305
  parser.set_submodule(self)
303
306
  return SectionConstraint(parser, sm_section(self.name))
304
307
 
308
+ @classmethod
309
+ def _validated_name(cls, name: str) -> str:
310
+ if (
311
+ not name
312
+ or name.startswith(("/", "\\"))
313
+ or ntpath.splitdrive(name)[0]
314
+ or ".." in name.replace("\\", "/").split("/")
315
+ ):
316
+ raise ValueError("Invalid submodule name %r" % name)
317
+ return name
318
+
305
319
  @classmethod
306
320
  def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
321
+ name = cls._validated_name(name)
307
322
  if cls._need_gitfile_submodules(parent_repo.git):
308
323
  return osp.join(parent_repo.git_dir, "modules", name)
309
324
  if parent_repo.working_tree_dir:
@@ -349,6 +364,15 @@ class Submodule(IndexObject, TraversableIterableObj):
349
364
  module_abspath = cls._module_abspath(repo, path, name)
350
365
  module_checkout_path = module_abspath
351
366
  if cls._need_gitfile_submodules(repo.git):
367
+ if not allow_unsafe_options:
368
+ Git.check_unsafe_options(Git._option_candidates([], kwargs), repo.unsafe_git_clone_options)
369
+ multi_options = kwargs.get("multi_options")
370
+ if multi_options:
371
+ Git.check_unsafe_options(
372
+ shlex.split(" ".join(cast("Sequence[str]", multi_options))),
373
+ repo.unsafe_git_clone_options,
374
+ )
375
+ allow_unsafe_options = True
352
376
  kwargs["separate_git_dir"] = module_abspath
353
377
  module_abspath_dir = osp.dirname(module_abspath)
354
378
  if not osp.isdir(module_abspath_dir):
@@ -379,23 +403,14 @@ class Submodule(IndexObject, TraversableIterableObj):
379
403
  :raise ValueError:
380
404
  If path is not contained in the parent repository's working tree.
381
405
  """
382
- path = to_native_path_linux(path)
406
+ if parent_repo.working_tree_dir:
407
+ path = _to_relative_path(parent_repo.working_tree_dir, path)
408
+ else:
409
+ path = to_native_path_linux(path)
383
410
  if path.endswith("/"):
384
411
  path = path[:-1]
385
- # END handle trailing slash
386
-
387
- if osp.isabs(path) and parent_repo.working_tree_dir:
388
- working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
389
- if not path.startswith(working_tree_linux):
390
- raise ValueError(
391
- "Submodule checkout path '%s' needs to be within the parents repository at '%s'"
392
- % (working_tree_linux, path)
393
- )
394
- path = path[len(working_tree_linux.rstrip("/")) + 1 :]
395
- if not path:
396
- raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
397
- # END verify converted relative path makes sense
398
- # END convert to a relative path
412
+ if not path or path == ".":
413
+ raise ValueError("Submodule checkout path must not be the repository root")
399
414
 
400
415
  return path
401
416
 
@@ -523,6 +538,7 @@ class Submodule(IndexObject, TraversableIterableObj):
523
538
  raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
524
539
  # END handle bare repos
525
540
 
541
+ cls._validated_name(name)
526
542
  path = cls._to_relative_path(repo, path)
527
543
 
528
544
  # Ensure we never put backslashes into the URL, as might happen on Windows.
@@ -734,9 +750,7 @@ class Submodule(IndexObject, TraversableIterableObj):
734
750
  prefix = "DRY-RUN: "
735
751
  # END handle prefix
736
752
 
737
- # To keep things plausible in dry-run mode.
738
- if dry_run:
739
- mrepo = None
753
+ mrepo = None
740
754
  # END init mrepo
741
755
 
742
756
  def fetch_remotes(module_repo: "Repo") -> None:
@@ -771,6 +785,8 @@ class Submodule(IndexObject, TraversableIterableObj):
771
785
  # END fetch new data
772
786
 
773
787
  try:
788
+ self._validated_name(self.name)
789
+
774
790
  # ENSURE REPO IS PRESENT AND UP-TO-DATE
775
791
  #######################################
776
792
  try:
@@ -871,6 +887,7 @@ class Submodule(IndexObject, TraversableIterableObj):
871
887
  #############################
872
888
  binsha = self.binsha
873
889
  hexsha = self.hexsha
890
+ is_detached = False
874
891
  if mrepo is not None:
875
892
  # mrepo is only set if we are not in dry-run mode or if the module
876
893
  # existed.
@@ -1020,6 +1037,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1020
1037
  raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1021
1038
  # END handle input
1022
1039
 
1040
+ self._validated_name(self.name)
1023
1041
  module_checkout_path = self._to_relative_path(self.repo, module_path)
1024
1042
 
1025
1043
  # VERIFY DESTINATION
@@ -1160,6 +1178,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1160
1178
  raise ValueError("Need to specify to delete at least the module, or the configuration")
1161
1179
  # END handle parameters
1162
1180
 
1181
+ self._validated_name(self.name)
1163
1182
  # Recursively remove children of this submodule.
1164
1183
  nc = 0
1165
1184
  for csm in self.children():
@@ -1213,6 +1232,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1213
1232
  for remote in mod.remotes:
1214
1233
  num_branches_with_new_commits = 0
1215
1234
  rrefs = remote.refs
1235
+ rref = None
1216
1236
  for rref in rrefs:
1217
1237
  num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1218
1238
  # END for each remote ref
@@ -1416,6 +1436,9 @@ class Submodule(IndexObject, TraversableIterableObj):
1416
1436
  if self.name == new_name:
1417
1437
  return self
1418
1438
 
1439
+ self._validated_name(self.name)
1440
+ self._validated_name(new_name)
1441
+
1419
1442
  # .git/config
1420
1443
  with self.repo.config_writer() as pw:
1421
1444
  # As we ourselves didn't write anything about submodules into the parent
@@ -1463,6 +1486,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1463
1486
  If a repository was not available.
1464
1487
  This could also mean that it was not yet initialized.
1465
1488
  """
1489
+ self._validated_name(self.name)
1466
1490
  module_checkout_abspath = self.abspath
1467
1491
  try:
1468
1492
  repo = git.Repo(module_checkout_abspath)
@@ -7,6 +7,7 @@ import logging
7
7
 
8
8
  import git
9
9
  from git.exc import InvalidGitRepositoryError
10
+ from git.util import IterableList
10
11
 
11
12
  from .base import Submodule, UpdateProgress
12
13
  from .util import find_first_remote_branch
@@ -19,7 +20,6 @@ from git.types import Commit_ish
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from git.repo import Repo
22
- from git.util import IterableList
23
23
 
24
24
  # ----------------------------------------------------------------------------
25
25
 
@@ -162,6 +162,7 @@ class RootModule(Submodule):
162
162
  prefix = "DRY-RUN: "
163
163
 
164
164
  repo = self.repo
165
+ sms: "IterableList[Submodule]" = IterableList("name")
165
166
 
166
167
  try:
167
168
  # SETUP BASE COMMIT
@@ -182,7 +183,7 @@ class RootModule(Submodule):
182
183
  # END handle previous commit
183
184
 
184
185
  psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
185
- sms: "IterableList[Submodule]" = self.list_items(repo)
186
+ sms = self.list_items(repo)
186
187
  spsms = set(psms)
187
188
  ssms = set(sms)
188
189
 
git/refs/head.py CHANGED
@@ -19,6 +19,7 @@ from .symbolic import SymbolicReference
19
19
 
20
20
  from typing import Any, Sequence, TYPE_CHECKING, Union
21
21
 
22
+ from git.cmd import Git
22
23
  from git.types import Commit_ish, PathLike
23
24
 
24
25
  if TYPE_CHECKING:
@@ -62,6 +63,7 @@ class HEAD(SymbolicReference):
62
63
  index: bool = True,
63
64
  working_tree: bool = False,
64
65
  paths: Union[PathLike, Sequence[PathLike], None] = None,
66
+ allow_unsafe_options: bool = False,
65
67
  **kwargs: Any,
66
68
  ) -> "HEAD":
67
69
  """Reset our HEAD to the given commit optionally synchronizing the index and
@@ -84,12 +86,21 @@ class HEAD(SymbolicReference):
84
86
  Single path or list of paths relative to the git root directory
85
87
  that are to be reset. This allows to partially reset individual files.
86
88
 
89
+ :param allow_unsafe_options:
90
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
91
+ :manpage:`git-reset(1)`.
92
+
87
93
  :param kwargs:
88
94
  Additional arguments passed to :manpage:`git-reset(1)`.
89
95
 
90
96
  :return:
91
97
  self
92
98
  """
99
+ if not allow_unsafe_options:
100
+ Git.check_unsafe_options(
101
+ options=Git._option_candidates([commit], kwargs),
102
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
103
+ )
93
104
  mode: Union[str, None]
94
105
  mode = "--soft"
95
106
  if index:
@@ -234,7 +245,12 @@ class Head(Reference):
234
245
  self.path = "%s/%s" % (self._common_path_default, new_path)
235
246
  return self
236
247
 
237
- def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
248
+ def checkout(
249
+ self,
250
+ force: bool = False,
251
+ allow_unsafe_options: bool = False,
252
+ **kwargs: Any,
253
+ ) -> Union["HEAD", "Head"]:
238
254
  """Check out this head by setting the HEAD to this reference, by updating the
239
255
  index to reflect the tree we point to and by updating the working tree to
240
256
  reflect the latest index.
@@ -246,6 +262,10 @@ class Head(Reference):
246
262
  If ``False``, :exc:`~git.exc.GitCommandError` will be raised in that
247
263
  situation.
248
264
 
265
+ :param allow_unsafe_options:
266
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
267
+ :manpage:`git-checkout(1)`.
268
+
249
269
  :param kwargs:
250
270
  Additional keyword arguments to be passed to git checkout, e.g.
251
271
  ``b="new_branch"`` to create a new branch at the given spot.
@@ -261,6 +281,11 @@ class Head(Reference):
261
281
  the HEAD detached which is allowed and possible, but remains a special state
262
282
  that some tools might not be able to handle.
263
283
  """
284
+ if not allow_unsafe_options:
285
+ Git.check_unsafe_options(
286
+ options=Git._option_candidates([], kwargs),
287
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
288
+ )
264
289
  kwargs["f"] = force
265
290
  if kwargs["f"] is False:
266
291
  kwargs.pop("f")
git/refs/log.py CHANGED
@@ -269,6 +269,7 @@ class RefLog(List[RefLogEntry], Serializable):
269
269
  return RefLogEntry.from_line(fp.readlines()[index].strip())
270
270
  # Read until index is reached.
271
271
 
272
+ line = b""
272
273
  for i in range(index + 1):
273
274
  line = fp.readline()
274
275
  if not line:
git/refs/symbolic.py CHANGED
@@ -119,7 +119,7 @@ class SymbolicReference:
119
119
  common_path = os.path.commonpath([base_path, abs_path])
120
120
  except ValueError as e:
121
121
  raise ValueError("Reference path %r escapes the repository" % path) from e
122
- if os.path.normcase(common_path) != os.path.normcase(base_path):
122
+ if common_path != base_path:
123
123
  raise ValueError("Reference path %r escapes the repository" % path)
124
124
  return abs_path
125
125
 
git/refs/tag.py CHANGED
@@ -134,15 +134,17 @@ class TagReference(Reference):
134
134
  :return:
135
135
  A new :class:`TagReference`.
136
136
  """
137
+ legacy_ref = kwargs.pop("ref", None)
138
+ if legacy_ref:
139
+ reference = legacy_ref
140
+
137
141
  if not allow_unsafe_options:
138
142
  Git.check_unsafe_options(
139
- options=Git._option_candidates([], kwargs),
143
+ options=Git._option_candidates([path, reference], kwargs),
140
144
  unsafe_options=cls.unsafe_git_tag_options,
145
+ clusterable_short_options="46adefilnqsv",
141
146
  )
142
147
 
143
- if "ref" in kwargs and kwargs["ref"]:
144
- reference = kwargs["ref"]
145
-
146
148
  if "message" in kwargs and kwargs["message"]:
147
149
  kwargs["m"] = kwargs["message"]
148
150
  del kwargs["message"]
git/repo/base.py CHANGED
@@ -142,6 +142,14 @@ class Repo:
142
142
  re_author_committer_start = re.compile(r"^(author|committer)")
143
143
  re_tab_full_line = re.compile(r"^\t(.*)$")
144
144
 
145
+ unsafe_git_init_options = [
146
+ # Can install hooks that execute during later Git commands:
147
+ "--template",
148
+ # Redirects the repository metadata to a caller-controlled path:
149
+ "--separate-git-dir",
150
+ ]
151
+ """Options to :manpage:`git-init(1)` that permit unsafe code execution or I/O."""
152
+
145
153
  unsafe_git_clone_options = [
146
154
  # Executes arbitrary commands:
147
155
  "--upload-pack",
@@ -151,6 +159,8 @@ class Repo:
151
159
  "-c",
152
160
  # Can install hooks that execute during clone:
153
161
  "--template",
162
+ # Redirects the repository metadata to a caller-controlled path:
163
+ "--separate-git-dir",
154
164
  # Fetches from an additional caller-controlled URI:
155
165
  "--bundle-uri",
156
166
  ]
@@ -191,6 +201,19 @@ class Repo:
191
201
  "-o",
192
202
  ]
193
203
 
204
+ unsafe_git_blame_options = unsafe_git_revision_options + [
205
+ # These options read from arbitrary files and expose their contents through blame output.
206
+ "--contents",
207
+ "-S",
208
+ "--ignore-revs-file",
209
+ ]
210
+
211
+ unsafe_git_diff_options = unsafe_git_revision_options + [
212
+ # Reads caller-controlled order patterns from an arbitrary file.
213
+ "-O",
214
+ "--orderfile",
215
+ ]
216
+
194
217
  # Invariants
195
218
  config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
196
219
  """Represents the configuration level of a configuration file."""
@@ -933,13 +956,17 @@ class Repo:
933
956
  return False
934
957
 
935
958
  def _get_daemon_export(self) -> bool:
936
- if self.git_dir:
937
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
959
+ git_dir = getattr(self, "git_dir", None)
960
+ if git_dir is None:
961
+ return False
962
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
938
963
  return osp.exists(filename)
939
964
 
940
965
  def _set_daemon_export(self, value: object) -> None:
941
- if self.git_dir:
942
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
966
+ git_dir = getattr(self, "git_dir", None)
967
+ if git_dir is None:
968
+ return
969
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
943
970
  fileexists = osp.exists(filename)
944
971
  if value and not fileexists:
945
972
  touch(filename)
@@ -961,8 +988,7 @@ class Repo:
961
988
  :return:
962
989
  List of strings being pathnames of alternates
963
990
  """
964
- if self.git_dir:
965
- alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
991
+ alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
966
992
 
967
993
  if osp.exists(alternates_path):
968
994
  with open(alternates_path, "rb") as f:
@@ -1138,7 +1164,7 @@ class Repo:
1138
1164
  :manpage:`git-rev-parse(1)` is a valid option.
1139
1165
 
1140
1166
  :param allow_unsafe_options:
1141
- Allow unsafe options in revision argument, like ``--output``.
1167
+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1142
1168
 
1143
1169
  :return:
1144
1170
  Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
@@ -1150,7 +1176,9 @@ class Repo:
1150
1176
  """
1151
1177
  if not allow_unsafe_options:
1152
1178
  Git.check_unsafe_options(
1153
- options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options
1179
+ options=Git._option_candidates([rev], kwargs),
1180
+ unsafe_options=self.unsafe_git_blame_options,
1181
+ clusterable_short_options="46bceflnpqstvw",
1154
1182
  )
1155
1183
 
1156
1184
  data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
@@ -1242,7 +1270,7 @@ class Repo:
1242
1270
  :manpage:`git-rev-parse(1)` is a valid option.
1243
1271
 
1244
1272
  :param allow_unsafe_options:
1245
- Allow unsafe options in revision argument, like ``--output``.
1273
+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1246
1274
 
1247
1275
  :return:
1248
1276
  list: [git.Commit, list: [<line>]]
@@ -1258,7 +1286,8 @@ class Repo:
1258
1286
  if not allow_unsafe_options:
1259
1287
  Git.check_unsafe_options(
1260
1288
  options=Git._option_candidates([rev, rev_opts_list], kwargs),
1261
- unsafe_options=self.unsafe_git_revision_options,
1289
+ unsafe_options=self.unsafe_git_blame_options,
1290
+ clusterable_short_options="46bceflnpqstvw",
1262
1291
  )
1263
1292
  data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs)
1264
1293
  commits: Dict[str, Commit] = {}
@@ -1280,6 +1309,7 @@ class Repo:
1280
1309
 
1281
1310
  keepends = True
1282
1311
  for line_bytes in data.splitlines(keepends):
1312
+ line_str = ""
1283
1313
  try:
1284
1314
  line_str = line_bytes.rstrip().decode(defenc)
1285
1315
  except UnicodeDecodeError:
@@ -1394,6 +1424,7 @@ class Repo:
1394
1424
  mkdir: bool = True,
1395
1425
  odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1396
1426
  expand_vars: bool = True,
1427
+ allow_unsafe_options: bool = False,
1397
1428
  **kwargs: Any,
1398
1429
  ) -> "Repo":
1399
1430
  """Initialize a git repository at the given path if specified.
@@ -1418,6 +1449,10 @@ class Repo:
1418
1449
  information disclosure, allowing attackers to access the contents of
1419
1450
  environment variables.
1420
1451
 
1452
+ :param allow_unsafe_options:
1453
+ Allow unsafe options to be used, such as ``--template`` and
1454
+ ``--separate-git-dir``.
1455
+
1421
1456
  :param kwargs:
1422
1457
  Keyword arguments serving as additional options to the
1423
1458
  :manpage:`git-init(1)` command.
@@ -1425,6 +1460,11 @@ class Repo:
1425
1460
  :return:
1426
1461
  :class:`Repo` (the newly created repo)
1427
1462
  """
1463
+ if not allow_unsafe_options:
1464
+ Git.check_unsafe_options(
1465
+ options=Git._option_candidates([], kwargs),
1466
+ unsafe_options=cls.unsafe_git_init_options,
1467
+ )
1428
1468
  if path:
1429
1469
  path = expand_path(path, expand_vars)
1430
1470
  if mkdir and path and not osp.exists(path):
@@ -1738,8 +1778,9 @@ class Repo:
1738
1778
 
1739
1779
  ``None`` if we are not currently rebasing.
1740
1780
  """
1741
- if self.git_dir:
1742
- rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1781
+ if not self.git_dir:
1782
+ return None
1783
+ rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1743
1784
  if not osp.isfile(rebase_head_file):
1744
1785
  return None
1745
1786
  with open(rebase_head_file, "rt") as f:
git/util.py CHANGED
@@ -315,6 +315,68 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
315
315
  return to_native_path(join_path(a, *p))
316
316
 
317
317
 
318
+ def _is_path_rooted(path: PathLike) -> bool:
319
+ r"""Whether ``path`` has a root, including one encoded in a UNC drive.
320
+
321
+ On Windows, ``\directory`` is rooted on the current drive without being
322
+ absolute, while ``C:\directory`` has both a drive and a root. In contrast,
323
+ ``directory`` and the drive-relative ``C:directory`` have no root.
324
+ UNC paths are rooted: ``\\server\share`` stores the share in the drive
325
+ returned by :func:`os.path.splitdrive`, while ``\\server\share\directory``
326
+ additionally has a rooted tail.
327
+ On POSIX, which has no drive concept, this simply distinguishes absolute
328
+ paths such as ``/directory`` from relative paths such as ``directory``.
329
+ """
330
+ drive, tail = osp.splitdrive(os.fspath(path))
331
+ separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
332
+ return tail.startswith(separators) or drive.startswith(separators)
333
+
334
+
335
+ def _to_relative_path(root: PathLike, path: PathLike) -> str:
336
+ r"""Return a normalized Git-style path confined to ``root``.
337
+
338
+ A Windows path such as ``\directory`` is rooted but not absolute. Resolve it
339
+ against the drive of ``root`` rather than treating it as relative to ``root``.
340
+ Drive-relative paths such as ``C:directory`` are rejected because their meaning
341
+ depends on process-global per-drive state.
342
+
343
+ For example, with ``root`` set to ``C:\repo`` on Windows:
344
+
345
+ * ``directory\file`` -> ``directory/file``
346
+ * ``directory\`` -> ``directory/``
347
+ * ``C:\repo\directory\file`` -> ``directory/file``
348
+ * ``\repo\directory\file`` -> ``directory/file``
349
+ * ``C:directory\file`` -> :exc:`ValueError`
350
+ * ``C:\other\file`` -> :exc:`ValueError`
351
+
352
+ On POSIX, ``/repo/directory/file`` under ``/repo`` similarly becomes
353
+ ``directory/file``. A trailing separator is preserved as a Git-style ``/``.
354
+ """
355
+ path_str = os.fspath(path)
356
+ if not path_str:
357
+ return path_str
358
+
359
+ drive, _tail = osp.splitdrive(path_str)
360
+ rooted = _is_path_rooted(path_str)
361
+ if drive and not rooted:
362
+ raise ValueError("Drive-relative path %r is not supported" % path_str)
363
+
364
+ root_abs = osp.abspath(os.fspath(root))
365
+ path_abs = osp.abspath(osp.join(root_abs, path_str))
366
+ try:
367
+ common_path = osp.commonpath([root_abs, path_abs])
368
+ except ValueError as e:
369
+ raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs)) from e
370
+ if common_path != root_abs:
371
+ raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs))
372
+
373
+ relative_path = to_native_path_linux(osp.relpath(path_abs, root_abs))
374
+ separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
375
+ if path_str.endswith(separators) and relative_path != "." and not relative_path.endswith("/"):
376
+ relative_path += "/"
377
+ return relative_path
378
+
379
+
318
380
  def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
319
381
  """Make sure that the directory pointed to by path exists.
320
382
 
@@ -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,44 +1,44 @@
1
- git/__init__.py,sha256=FxGLSM_UucTDAQ6NhNZ1vs-H_GoxqUQWjb5fum6nmpg,8899
2
- git/cmd.py,sha256=G-SH2Q_mXjA8P54qY7nr5rRF00-cGtOcb2QKsoaZ9pQ,76423
1
+ git/__init__.py,sha256=RQ8_0lssQ6Httk5nXlJwIji5mDSzx_28lTUwZkbXfiA,8899
2
+ git/cmd.py,sha256=y-7TVp8PVWVkXcQrKqrSO2vaD6Bdiue73ZzFlWzLryU,77137
3
3
  git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
4
- git/config.py,sha256=aLDSuDiiB67W-OLktJpYVjSLwgIqWJtPYbRllNhQ7Gw,38406
5
- git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
6
- git/diff.py,sha256=k7iYB8gBzqrGDNn_XfZPXpS6LFxq02-JnyLo4bdwvVA,28271
4
+ git/config.py,sha256=0fc1F5G_S6CksbO2-pTaB5bspNyyPidYhrEhwD9mvCs,39261
5
+ git/db.py,sha256=X19omm9sk3eEy5WAzMoZ2tG9UmdPZFQYf-8yqpvsYF0,3202
6
+ git/diff.py,sha256=QJylerwXENN87gQ30iJ3KbOIoCHFH27QPeKPgUsw5C8,28363
7
7
  git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
8
8
  git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  git/remote.py,sha256=d68K7XmkYXZUlIoLBDRpRom_df6mbkhWMrcPAP5s11c,46900
10
10
  git/types.py,sha256=d3oDduUGjfoqffcUL_GOD-3qroF8m3r9ai6Xi9_FraM,10279
11
- git/util.py,sha256=ECVUNHcGY7_UTeak3ziyEAuDO20YGEOBxuEC8goD15g,45325
11
+ git/util.py,sha256=TJWZ8zeqpsp2NoIr5u_tcUB7yM3OB_RHoYLy4b95TUQ,48135
12
12
  git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
13
- git/index/base.py,sha256=LSrCisdF_RnBtDMSvX7tKmWUB8l7AhgwWv2bFw0H0f0,63237
14
- git/index/fun.py,sha256=Cfrpq6GmhLXGVDDjYiXTN4f4bb35kvw6-cOGOBuJeGc,17862
13
+ git/index/base.py,sha256=cOeIjjS2LYZwqKdSGvrcgVC37TNZcTNtQzHWbGPyRwk,64775
14
+ git/index/fun.py,sha256=AIW3T6Gr6h5E22z_2wKHevzh5K0HhjjEO9kXAK3nHOM,22419
15
15
  git/index/typ.py,sha256=ZjRWp5J79OwFQor1XXN4C6JmHTDclD9H-tKZtAeqyTI,7019
16
16
  git/index/util.py,sha256=f0BfE2JPkdypsB2Q_HJ8KU7MC45RRHywL_GWwxa_5hw,3676
17
17
  git/objects/__init__.py,sha256=O6ZL_olX7e5-8iIbKviRPkVSJxN37WA-EC0q9d48U5Y,637
18
18
  git/objects/base.py,sha256=5-p9uSGWvPxX9hidvkGH9tQOJoH2eaRggUbMr9VJiL0,10285
19
19
  git/objects/blob.py,sha256=G9PZMNc5CFRQFNnlquBRAM5-jZg8581lRcxkvARobSk,1231
20
- git/objects/commit.py,sha256=aJYxpbkcmAskmnc6qv6Podswhshu1-GlY306UqNWzvs,33175
20
+ git/objects/commit.py,sha256=YbF2cjl6aziTq2HRgZkxr5TgfU8keIVEEkoBB0DPLKc,33291
21
21
  git/objects/fun.py,sha256=bcofjsxrF1NniYWa1r8oAy8uYT_q2rUpLFnYj1aULGU,8949
22
22
  git/objects/tag.py,sha256=jAGESnpmTEv-dLakPzheT5ILZFFArcItnXYqfxfDrgc,4441
23
23
  git/objects/tree.py,sha256=MmExcS59Iamb2cBqUvoHPdzgixabyvv5voHabossrl8,13898
24
24
  git/objects/util.py,sha256=Nlza4zLgdPmr_Yasyvvs6c1rKtW_wMxI6wDmQpQ3ufw,23846
25
25
  git/objects/submodule/__init__.py,sha256=6xySp767LVz3UylWgUalntS_nGXRuVzXxDuFAv_Wc2c,303
26
- git/objects/submodule/base.py,sha256=1w39BpgNnpEmEacCLdCGLH-aeIaAMKU0oWUtgVcyVW0,66347
27
- git/objects/submodule/root.py,sha256=5eTtYNHasqdPq6q0oDCPr7IaO6uAHL3b4DxMoiO2LhE,20246
26
+ git/objects/submodule/base.py,sha256=JBoT8FiVCk9ODMMQo2DDR89ThZDmj317Qr2L4JZtB2g,67073
27
+ git/objects/submodule/root.py,sha256=767GIhBwCwn2xvXeMHm2uEO3kZlRyaIaFXnSqcfdZxU,20277
28
28
  git/objects/submodule/util.py,sha256=sQqAYaiSJdFkZa9NlAuK_wTsMNiS-kkQnQjvIoJtc_o,3509
29
29
  git/refs/__init__.py,sha256=DWlJNnsx-4jM_E-VycbP-FZUdn6iWhjnH_uZ_pZXBro,509
30
- git/refs/head.py,sha256=59MUMVu9phmE_ImkmNw0sDDSCc75s7TxkQj_cKzRhbo,10397
31
- git/refs/log.py,sha256=X88JTqjwX1ShvHr-NHNAZj0qLiHbVoF-0v3VymZDwoM,12612
30
+ git/refs/head.py,sha256=Q3VZt5MmEUwltBpBHJmcAZM9gYzCekAKgMpJlPrbttk,11309
31
+ git/refs/log.py,sha256=G2e9GR1owwdmDuKmwGGozt5b1y8qIccUoXiuk2q1E1o,12635
32
32
  git/refs/reference.py,sha256=uzrvNTZ5fg7A0Aq4VC3KLSMbksgYUf4KvmHau0tFYIM,5646
33
33
  git/refs/remote.py,sha256=WTlkJtpu895jJxbknDNOwL31tivQY_AHQrvroUoFBX4,2902
34
- git/refs/symbolic.py,sha256=7ScmZHwlqPDKX9bxVC5queSZXUgqEY7_J_Si6gDBSLw,36139
35
- git/refs/tag.py,sha256=27fZPVVkAPvaRoUt6_IX-tXKLleCYwoKA0GAk453w9E,5418
34
+ git/refs/symbolic.py,sha256=MPi5UN0ujdE2GMOrzfW2zt-6HlVreuY5W4P7tnBg4V8,36103
35
+ git/refs/tag.py,sha256=Gm1jn9kClc6f6jGQXRrpLyelrM3-KDcqHRRkD0mL4SE,5510
36
36
  git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
37
- git/repo/base.py,sha256=sCYS-QC5Dan98FYQgqnOFID0J6HzS8rTJf7Ea8i5DWg,64565
37
+ git/repo/base.py,sha256=OrH--uFUNqz5XgK-vQou3LxoOdr1wGRtej3kh0zJN8c,66119
38
38
  git/repo/fun.py,sha256=PDFNkuTSGGoQc0EEEXjxHw2_KaKYvVjBZ9rrkwH5Q6Y,23462
39
- gitpython-3.1.57.dist-info/licenses/AUTHORS,sha256=Iucx_CH3w7_4084yjM6C5BvKfLA8cVfYHWW2pVmpxSk,2410
40
- gitpython-3.1.57.dist-info/licenses/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
41
- gitpython-3.1.57.dist-info/METADATA,sha256=3rZoSxhgf4E9RPU4L4agiWkTRO3f54fjSQcZcKZmzXo,13558
42
- gitpython-3.1.57.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
43
- gitpython-3.1.57.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
44
- gitpython-3.1.57.dist-info/RECORD,,
39
+ gitpython-3.1.59.dist-info/licenses/AUTHORS,sha256=Iucx_CH3w7_4084yjM6C5BvKfLA8cVfYHWW2pVmpxSk,2410
40
+ gitpython-3.1.59.dist-info/licenses/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
41
+ gitpython-3.1.59.dist-info/METADATA,sha256=x3hW_O_MEvdlUzGbI_Nb4eaXbr00unmxiHwLTIu87ZA,13558
42
+ gitpython-3.1.59.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
43
+ gitpython-3.1.59.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
44
+ gitpython-3.1.59.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5