GitPython 3.1.57__tar.gz → 3.1.58__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.58/GitPython.egg-info}/PKG-INFO +1 -1
  2. {gitpython-3.1.57/GitPython.egg-info → gitpython-3.1.58}/PKG-INFO +1 -1
  3. gitpython-3.1.58/VERSION +1 -0
  4. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/changes.rst +20 -2
  5. {gitpython-3.1.57 → gitpython-3.1.58}/git/__init__.py +1 -1
  6. {gitpython-3.1.57 → gitpython-3.1.58}/git/cmd.py +16 -2
  7. {gitpython-3.1.57 → gitpython-3.1.58}/git/config.py +12 -4
  8. {gitpython-3.1.57 → gitpython-3.1.58}/git/index/base.py +62 -16
  9. {gitpython-3.1.57 → gitpython-3.1.58}/git/index/fun.py +100 -2
  10. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/commit.py +5 -2
  11. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/submodule/base.py +32 -18
  12. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/submodule/root.py +3 -2
  13. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/head.py +26 -1
  14. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/log.py +1 -0
  15. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/symbolic.py +1 -1
  16. {gitpython-3.1.57 → gitpython-3.1.58}/git/repo/base.py +31 -8
  17. {gitpython-3.1.57 → gitpython-3.1.58}/git/util.py +62 -0
  18. {gitpython-3.1.57 → gitpython-3.1.58}/pyproject.toml +2 -2
  19. gitpython-3.1.57/VERSION +0 -1
  20. {gitpython-3.1.57 → gitpython-3.1.58}/AUTHORS +0 -0
  21. {gitpython-3.1.57 → gitpython-3.1.58}/CHANGES +0 -0
  22. {gitpython-3.1.57 → gitpython-3.1.58}/CONTRIBUTING.md +0 -0
  23. {gitpython-3.1.57 → gitpython-3.1.58}/GitPython.egg-info/SOURCES.txt +0 -0
  24. {gitpython-3.1.57 → gitpython-3.1.58}/GitPython.egg-info/dependency_links.txt +0 -0
  25. {gitpython-3.1.57 → gitpython-3.1.58}/GitPython.egg-info/not-zip-safe +0 -0
  26. {gitpython-3.1.57 → gitpython-3.1.58}/GitPython.egg-info/requires.txt +0 -0
  27. {gitpython-3.1.57 → gitpython-3.1.58}/GitPython.egg-info/top_level.txt +0 -0
  28. {gitpython-3.1.57 → gitpython-3.1.58}/LICENSE +0 -0
  29. {gitpython-3.1.57 → gitpython-3.1.58}/MANIFEST.in +0 -0
  30. {gitpython-3.1.57 → gitpython-3.1.58}/README.md +0 -0
  31. {gitpython-3.1.57 → gitpython-3.1.58}/doc/.gitignore +0 -0
  32. {gitpython-3.1.57 → gitpython-3.1.58}/doc/Makefile +0 -0
  33. {gitpython-3.1.57 → gitpython-3.1.58}/doc/requirements.txt +0 -0
  34. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/conf.py +0 -0
  35. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/index.rst +0 -0
  36. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/intro.rst +0 -0
  37. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/quickstart.rst +0 -0
  38. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/reference.rst +0 -0
  39. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/roadmap.rst +0 -0
  40. {gitpython-3.1.57 → gitpython-3.1.58}/doc/source/tutorial.rst +0 -0
  41. {gitpython-3.1.57 → gitpython-3.1.58}/git/compat.py +0 -0
  42. {gitpython-3.1.57 → gitpython-3.1.58}/git/db.py +0 -0
  43. {gitpython-3.1.57 → gitpython-3.1.58}/git/diff.py +0 -0
  44. {gitpython-3.1.57 → gitpython-3.1.58}/git/exc.py +0 -0
  45. {gitpython-3.1.57 → gitpython-3.1.58}/git/index/__init__.py +0 -0
  46. {gitpython-3.1.57 → gitpython-3.1.58}/git/index/typ.py +0 -0
  47. {gitpython-3.1.57 → gitpython-3.1.58}/git/index/util.py +0 -0
  48. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/__init__.py +0 -0
  49. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/base.py +0 -0
  50. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/blob.py +0 -0
  51. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/fun.py +0 -0
  52. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/submodule/__init__.py +0 -0
  53. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/submodule/util.py +0 -0
  54. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/tag.py +0 -0
  55. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/tree.py +0 -0
  56. {gitpython-3.1.57 → gitpython-3.1.58}/git/objects/util.py +0 -0
  57. {gitpython-3.1.57 → gitpython-3.1.58}/git/py.typed +0 -0
  58. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/__init__.py +0 -0
  59. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/reference.py +0 -0
  60. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/remote.py +0 -0
  61. {gitpython-3.1.57 → gitpython-3.1.58}/git/refs/tag.py +0 -0
  62. {gitpython-3.1.57 → gitpython-3.1.58}/git/remote.py +0 -0
  63. {gitpython-3.1.57 → gitpython-3.1.58}/git/repo/__init__.py +0 -0
  64. {gitpython-3.1.57 → gitpython-3.1.58}/git/repo/fun.py +0 -0
  65. {gitpython-3.1.57 → gitpython-3.1.58}/git/types.py +0 -0
  66. {gitpython-3.1.57 → gitpython-3.1.58}/requirements.txt +0 -0
  67. {gitpython-3.1.57 → gitpython-3.1.58}/setup.cfg +0 -0
  68. {gitpython-3.1.57 → gitpython-3.1.58}/setup.py +0 -0
  69. {gitpython-3.1.57 → gitpython-3.1.58}/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.58
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.58
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.58
@@ -2,6 +2,24 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 3.1.58
6
+ ======
7
+
8
+ Security fixes for
9
+
10
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hh9p-6wh2-4mfc
11
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-9rj7-rf2p-w77r
12
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-4gmw-gg2m-w46p
13
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-wvpp-8hx9-p66j
14
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-jm78-9fvv-mhgr
15
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hmq2-w58f-27jc
16
+
17
+ If you can, also try and provide feedback on the upcoming v4 branch
18
+ https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
19
+
20
+ See the following for all changes.
21
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.58
22
+
5
23
  3.1.57
6
24
  ======
7
25
 
@@ -14,7 +32,7 @@ If you can, also try and provide feedback on the upcoming v4 branch
14
32
  https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
15
33
 
16
34
  See the following for all changes.
17
- https://github.com/gitpython-developers/GitPython/releases/tag/3.1.55
35
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.57
18
36
 
19
37
  3.1.56
20
38
  ======
@@ -27,7 +45,7 @@ If you can, also try and provide feedback on the upcoming v4 branch
27
45
  https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
28
46
 
29
47
  See the following for all changes.
30
- https://github.com/gitpython-developers/GitPython/releases/tag/3.1.55
48
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.56
31
49
 
32
50
  3.1.55
33
51
  ======
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.57'
89
+ __version__ = '3.1.58'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
@@ -654,6 +654,12 @@ class Git(metaclass=_GitMeta):
654
654
  "--upload-pack",
655
655
  ]
656
656
 
657
+ unsafe_git_pathspec_from_file_options = [
658
+ # Reads pathspecs from a caller-controlled file. Some commands include an
659
+ # unmatched pathspec in their error output, which can disclose the file.
660
+ "--pathspec-from-file",
661
+ ]
662
+
657
663
  def __getstate__(self) -> Dict[str, Any]:
658
664
  return slots_to_dict(self, exclude=self._excluded_)
659
665
 
@@ -1044,13 +1050,21 @@ class Git(metaclass=_GitMeta):
1044
1050
  values = value if isinstance(value, (list, tuple)) else (value,)
1045
1051
  if any(value is True or (value is not False and value is not None) for value in values):
1046
1052
  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:
1053
+ if len(key) != 1:
1054
+ options.append(f"--{dashify(key)}")
1055
+ elif split_single_char_options:
1056
+ options.append(f"-{key}")
1049
1057
  options.extend(
1050
1058
  str(value)
1051
1059
  for value in values
1052
1060
  if value is not True and value not in (False, None) and str(value).startswith("-")
1053
1061
  )
1062
+ else:
1063
+ options.extend(
1064
+ f"-{key}" if value is True else f"-{key}{value}"
1065
+ for value in values
1066
+ if value is True or (value is not False and value is not None)
1067
+ )
1054
1068
  return options
1055
1069
 
1056
1070
  AutoInterrupt: TypeAlias = _AutoInterrupt
@@ -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:
@@ -897,6 +903,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
897
903
  def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None:
898
904
  if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):
899
905
  raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label)
906
+ if label == "option" and isinstance(name, str) and not VALID_CONFIG_OPTION_NAME_RE.fullmatch(name):
907
+ raise ValueError("Git config option names may contain only letters, digits, '-', '_', or '.'")
900
908
  if label == "section" and isinstance(name, str):
901
909
  in_quotes = False
902
910
  escaped = False
@@ -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.
@@ -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:
@@ -6,6 +6,7 @@ __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
11
12
  import stat
@@ -28,6 +29,7 @@ from git.objects.util import TraversableIterableObj
28
29
  from git.util import (
29
30
  IterableList,
30
31
  RemoteProgress,
32
+ _to_relative_path,
31
33
  join_path_native,
32
34
  rmtree,
33
35
  to_native_path_linux,
@@ -302,8 +304,20 @@ class Submodule(IndexObject, TraversableIterableObj):
302
304
  parser.set_submodule(self)
303
305
  return SectionConstraint(parser, sm_section(self.name))
304
306
 
307
+ @classmethod
308
+ def _validated_name(cls, name: str) -> str:
309
+ if (
310
+ not name
311
+ or name.startswith(("/", "\\"))
312
+ or ntpath.splitdrive(name)[0]
313
+ or ".." in name.replace("\\", "/").split("/")
314
+ ):
315
+ raise ValueError("Invalid submodule name %r" % name)
316
+ return name
317
+
305
318
  @classmethod
306
319
  def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
320
+ name = cls._validated_name(name)
307
321
  if cls._need_gitfile_submodules(parent_repo.git):
308
322
  return osp.join(parent_repo.git_dir, "modules", name)
309
323
  if parent_repo.working_tree_dir:
@@ -379,23 +393,14 @@ class Submodule(IndexObject, TraversableIterableObj):
379
393
  :raise ValueError:
380
394
  If path is not contained in the parent repository's working tree.
381
395
  """
382
- path = to_native_path_linux(path)
396
+ if parent_repo.working_tree_dir:
397
+ path = _to_relative_path(parent_repo.working_tree_dir, path)
398
+ else:
399
+ path = to_native_path_linux(path)
383
400
  if path.endswith("/"):
384
401
  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
402
+ if not path or path == ".":
403
+ raise ValueError("Submodule checkout path must not be the repository root")
399
404
 
400
405
  return path
401
406
 
@@ -523,6 +528,7 @@ class Submodule(IndexObject, TraversableIterableObj):
523
528
  raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
524
529
  # END handle bare repos
525
530
 
531
+ cls._validated_name(name)
526
532
  path = cls._to_relative_path(repo, path)
527
533
 
528
534
  # Ensure we never put backslashes into the URL, as might happen on Windows.
@@ -734,9 +740,7 @@ class Submodule(IndexObject, TraversableIterableObj):
734
740
  prefix = "DRY-RUN: "
735
741
  # END handle prefix
736
742
 
737
- # To keep things plausible in dry-run mode.
738
- if dry_run:
739
- mrepo = None
743
+ mrepo = None
740
744
  # END init mrepo
741
745
 
742
746
  def fetch_remotes(module_repo: "Repo") -> None:
@@ -771,6 +775,8 @@ class Submodule(IndexObject, TraversableIterableObj):
771
775
  # END fetch new data
772
776
 
773
777
  try:
778
+ self._validated_name(self.name)
779
+
774
780
  # ENSURE REPO IS PRESENT AND UP-TO-DATE
775
781
  #######################################
776
782
  try:
@@ -871,6 +877,7 @@ class Submodule(IndexObject, TraversableIterableObj):
871
877
  #############################
872
878
  binsha = self.binsha
873
879
  hexsha = self.hexsha
880
+ is_detached = False
874
881
  if mrepo is not None:
875
882
  # mrepo is only set if we are not in dry-run mode or if the module
876
883
  # existed.
@@ -1020,6 +1027,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1020
1027
  raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1021
1028
  # END handle input
1022
1029
 
1030
+ self._validated_name(self.name)
1023
1031
  module_checkout_path = self._to_relative_path(self.repo, module_path)
1024
1032
 
1025
1033
  # VERIFY DESTINATION
@@ -1160,6 +1168,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1160
1168
  raise ValueError("Need to specify to delete at least the module, or the configuration")
1161
1169
  # END handle parameters
1162
1170
 
1171
+ self._validated_name(self.name)
1163
1172
  # Recursively remove children of this submodule.
1164
1173
  nc = 0
1165
1174
  for csm in self.children():
@@ -1213,6 +1222,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1213
1222
  for remote in mod.remotes:
1214
1223
  num_branches_with_new_commits = 0
1215
1224
  rrefs = remote.refs
1225
+ rref = None
1216
1226
  for rref in rrefs:
1217
1227
  num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1218
1228
  # END for each remote ref
@@ -1416,6 +1426,9 @@ class Submodule(IndexObject, TraversableIterableObj):
1416
1426
  if self.name == new_name:
1417
1427
  return self
1418
1428
 
1429
+ self._validated_name(self.name)
1430
+ self._validated_name(new_name)
1431
+
1419
1432
  # .git/config
1420
1433
  with self.repo.config_writer() as pw:
1421
1434
  # As we ourselves didn't write anything about submodules into the parent
@@ -1463,6 +1476,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1463
1476
  If a repository was not available.
1464
1477
  This could also mean that it was not yet initialized.
1465
1478
  """
1479
+ self._validated_name(self.name)
1466
1480
  module_checkout_abspath = self.abspath
1467
1481
  try:
1468
1482
  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
 
@@ -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")
@@ -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:
@@ -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
 
@@ -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",
@@ -933,13 +941,17 @@ class Repo:
933
941
  return False
934
942
 
935
943
  def _get_daemon_export(self) -> bool:
936
- if self.git_dir:
937
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
944
+ git_dir = getattr(self, "git_dir", None)
945
+ if git_dir is None:
946
+ return False
947
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
938
948
  return osp.exists(filename)
939
949
 
940
950
  def _set_daemon_export(self, value: object) -> None:
941
- if self.git_dir:
942
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
951
+ git_dir = getattr(self, "git_dir", None)
952
+ if git_dir is None:
953
+ return
954
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
943
955
  fileexists = osp.exists(filename)
944
956
  if value and not fileexists:
945
957
  touch(filename)
@@ -961,8 +973,7 @@ class Repo:
961
973
  :return:
962
974
  List of strings being pathnames of alternates
963
975
  """
964
- if self.git_dir:
965
- alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
976
+ alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
966
977
 
967
978
  if osp.exists(alternates_path):
968
979
  with open(alternates_path, "rb") as f:
@@ -1280,6 +1291,7 @@ class Repo:
1280
1291
 
1281
1292
  keepends = True
1282
1293
  for line_bytes in data.splitlines(keepends):
1294
+ line_str = ""
1283
1295
  try:
1284
1296
  line_str = line_bytes.rstrip().decode(defenc)
1285
1297
  except UnicodeDecodeError:
@@ -1394,6 +1406,7 @@ class Repo:
1394
1406
  mkdir: bool = True,
1395
1407
  odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1396
1408
  expand_vars: bool = True,
1409
+ allow_unsafe_options: bool = False,
1397
1410
  **kwargs: Any,
1398
1411
  ) -> "Repo":
1399
1412
  """Initialize a git repository at the given path if specified.
@@ -1418,6 +1431,10 @@ class Repo:
1418
1431
  information disclosure, allowing attackers to access the contents of
1419
1432
  environment variables.
1420
1433
 
1434
+ :param allow_unsafe_options:
1435
+ Allow unsafe options to be used, such as ``--template`` and
1436
+ ``--separate-git-dir``.
1437
+
1421
1438
  :param kwargs:
1422
1439
  Keyword arguments serving as additional options to the
1423
1440
  :manpage:`git-init(1)` command.
@@ -1425,6 +1442,11 @@ class Repo:
1425
1442
  :return:
1426
1443
  :class:`Repo` (the newly created repo)
1427
1444
  """
1445
+ if not allow_unsafe_options:
1446
+ Git.check_unsafe_options(
1447
+ options=Git._option_candidates([], kwargs),
1448
+ unsafe_options=cls.unsafe_git_init_options,
1449
+ )
1428
1450
  if path:
1429
1451
  path = expand_path(path, expand_vars)
1430
1452
  if mkdir and path and not osp.exists(path):
@@ -1738,8 +1760,9 @@ class Repo:
1738
1760
 
1739
1761
  ``None`` if we are not currently rebasing.
1740
1762
  """
1741
- if self.git_dir:
1742
- rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1763
+ if not self.git_dir:
1764
+ return None
1765
+ rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1743
1766
  if not osp.isfile(rebase_head_file):
1744
1767
  return None
1745
1768
  with open(rebase_head_file, "rt") as f:
@@ -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
 
@@ -3,7 +3,7 @@ requires = ["setuptools"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [tool.pytest.ini_options]
6
- addopts = "--cov=git --cov-report=term -ra"
6
+ addopts = "--cov=git --cov-report=term -rfEX"
7
7
  filterwarnings = "ignore::DeprecationWarning"
8
8
  python_files = "test_*.py"
9
9
  tmp_path_retention_policy = "failed"
@@ -13,7 +13,7 @@ testpaths = "test" # Space separated list of paths from root e.g test tests doc
13
13
  # --cov-report term-missing # to terminal with line numbers
14
14
  # --cov-report html:path # html file at path
15
15
  # --maxfail # number of errors before giving up
16
- # -rfE # default test summary: list fail and error
16
+ # -rfEX # test summary: list failures, errors, and unexpected passes; omit expected failures
17
17
  # -ra # test summary: list all non-passing (fail, error, skip, xfail, xpass)
18
18
  # --ignore-glob=**/gitdb/* # ignore glob paths
19
19
  # filterwarnings ignore::WarningType # ignores those warnings
gitpython-3.1.57/VERSION DELETED
@@ -1 +0,0 @@
1
- 3.1.57
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