GitPython 3.1.58__tar.gz → 3.1.60__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.58 → gitpython-3.1.60/GitPython.egg-info}/PKG-INFO +1 -1
  2. {gitpython-3.1.58/GitPython.egg-info → gitpython-3.1.60}/PKG-INFO +1 -1
  3. gitpython-3.1.60/VERSION +1 -0
  4. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/changes.rst +32 -0
  5. {gitpython-3.1.58 → gitpython-3.1.60}/git/__init__.py +1 -1
  6. {gitpython-3.1.58 → gitpython-3.1.60}/git/cmd.py +7 -4
  7. {gitpython-3.1.58 → gitpython-3.1.60}/git/config.py +27 -6
  8. {gitpython-3.1.58 → gitpython-3.1.60}/git/db.py +24 -1
  9. {gitpython-3.1.58 → gitpython-3.1.60}/git/diff.py +33 -13
  10. {gitpython-3.1.58 → gitpython-3.1.60}/git/index/base.py +2 -1
  11. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/submodule/base.py +11 -1
  12. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/tag.py +6 -4
  13. {gitpython-3.1.58 → gitpython-3.1.60}/git/repo/base.py +86 -53
  14. {gitpython-3.1.58 → gitpython-3.1.60}/git/repo/fun.py +71 -39
  15. {gitpython-3.1.58 → gitpython-3.1.60}/git/util.py +8 -16
  16. gitpython-3.1.58/VERSION +0 -1
  17. {gitpython-3.1.58 → gitpython-3.1.60}/AUTHORS +0 -0
  18. {gitpython-3.1.58 → gitpython-3.1.60}/CHANGES +0 -0
  19. {gitpython-3.1.58 → gitpython-3.1.60}/CONTRIBUTING.md +0 -0
  20. {gitpython-3.1.58 → gitpython-3.1.60}/GitPython.egg-info/SOURCES.txt +0 -0
  21. {gitpython-3.1.58 → gitpython-3.1.60}/GitPython.egg-info/dependency_links.txt +0 -0
  22. {gitpython-3.1.58 → gitpython-3.1.60}/GitPython.egg-info/not-zip-safe +0 -0
  23. {gitpython-3.1.58 → gitpython-3.1.60}/GitPython.egg-info/requires.txt +0 -0
  24. {gitpython-3.1.58 → gitpython-3.1.60}/GitPython.egg-info/top_level.txt +0 -0
  25. {gitpython-3.1.58 → gitpython-3.1.60}/LICENSE +0 -0
  26. {gitpython-3.1.58 → gitpython-3.1.60}/MANIFEST.in +0 -0
  27. {gitpython-3.1.58 → gitpython-3.1.60}/README.md +0 -0
  28. {gitpython-3.1.58 → gitpython-3.1.60}/doc/.gitignore +0 -0
  29. {gitpython-3.1.58 → gitpython-3.1.60}/doc/Makefile +0 -0
  30. {gitpython-3.1.58 → gitpython-3.1.60}/doc/requirements.txt +0 -0
  31. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/conf.py +0 -0
  32. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/index.rst +0 -0
  33. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/intro.rst +0 -0
  34. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/quickstart.rst +0 -0
  35. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/reference.rst +0 -0
  36. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/roadmap.rst +0 -0
  37. {gitpython-3.1.58 → gitpython-3.1.60}/doc/source/tutorial.rst +0 -0
  38. {gitpython-3.1.58 → gitpython-3.1.60}/git/compat.py +0 -0
  39. {gitpython-3.1.58 → gitpython-3.1.60}/git/exc.py +0 -0
  40. {gitpython-3.1.58 → gitpython-3.1.60}/git/index/__init__.py +0 -0
  41. {gitpython-3.1.58 → gitpython-3.1.60}/git/index/fun.py +0 -0
  42. {gitpython-3.1.58 → gitpython-3.1.60}/git/index/typ.py +0 -0
  43. {gitpython-3.1.58 → gitpython-3.1.60}/git/index/util.py +0 -0
  44. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/__init__.py +0 -0
  45. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/base.py +0 -0
  46. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/blob.py +0 -0
  47. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/commit.py +0 -0
  48. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/fun.py +0 -0
  49. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/submodule/__init__.py +0 -0
  50. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/submodule/root.py +0 -0
  51. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/submodule/util.py +0 -0
  52. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/tag.py +0 -0
  53. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/tree.py +0 -0
  54. {gitpython-3.1.58 → gitpython-3.1.60}/git/objects/util.py +0 -0
  55. {gitpython-3.1.58 → gitpython-3.1.60}/git/py.typed +0 -0
  56. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/__init__.py +0 -0
  57. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/head.py +0 -0
  58. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/log.py +0 -0
  59. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/reference.py +0 -0
  60. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/remote.py +0 -0
  61. {gitpython-3.1.58 → gitpython-3.1.60}/git/refs/symbolic.py +0 -0
  62. {gitpython-3.1.58 → gitpython-3.1.60}/git/remote.py +0 -0
  63. {gitpython-3.1.58 → gitpython-3.1.60}/git/repo/__init__.py +0 -0
  64. {gitpython-3.1.58 → gitpython-3.1.60}/git/types.py +0 -0
  65. {gitpython-3.1.58 → gitpython-3.1.60}/pyproject.toml +0 -0
  66. {gitpython-3.1.58 → gitpython-3.1.60}/requirements.txt +0 -0
  67. {gitpython-3.1.58 → gitpython-3.1.60}/setup.cfg +0 -0
  68. {gitpython-3.1.58 → gitpython-3.1.60}/setup.py +0 -0
  69. {gitpython-3.1.58 → gitpython-3.1.60}/test-requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.58
3
+ Version: 3.1.60
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.58
3
+ Version: 3.1.60
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.60
@@ -2,6 +2,38 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 3.1.60
6
+ ======
7
+
8
+ Security fixes for
9
+
10
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-g5vv-9gxw-82hx
11
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-whh4-5q6c-9v3x
12
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-239g-whfq-7xj9
13
+
14
+ If you can, also try and provide feedback on the upcoming v4 branch
15
+ https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
16
+
17
+ See the following for all changes.
18
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.60
19
+
20
+ 3.1.59
21
+ ======
22
+
23
+ Security fixes for
24
+
25
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-5xxx-qhh7-9287
26
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3wxw-xv34-2frg
27
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-8mcc-hrx5-hvxc
28
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-284h-m62q-gf8w
29
+ * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-7833-fr7j-v32q
30
+
31
+ If you can, also try and provide feedback on the upcoming v4 branch
32
+ https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
33
+
34
+ See the following for all changes.
35
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.59
36
+
5
37
  3.1.58
6
38
  ======
7
39
 
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.58'
89
+ __version__ = '3.1.60'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
@@ -652,6 +652,7 @@ 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",
655
656
  ]
656
657
 
657
658
  unsafe_git_pathspec_from_file_options = [
@@ -976,7 +977,9 @@ class Git(metaclass=_GitMeta):
976
977
  return dashify(option_tokens[0])
977
978
 
978
979
  @classmethod
979
- 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:
980
983
  """Raise :class:`~git.exc.UnsafeOptionError` for blocked option spellings.
981
984
 
982
985
  In addition to exact matches, this rejects abbreviated long options accepted
@@ -1011,7 +1014,7 @@ class Git(metaclass=_GitMeta):
1011
1014
  # These value-less Git flags can be clustered before another short option
1012
1015
  # (for example, ``-fuVALUE``). Stop at any other character because it may
1013
1016
  # begin an attached value, as ``o`` does in the safe option ``-oupstream``.
1014
- clusterable_short_options = frozenset("46flnqsv")
1017
+ clusterable_short_options_set = frozenset(clusterable_short_options)
1015
1018
  options_are_kwargs = all(not option.startswith("-") for option in options)
1016
1019
  for option in options:
1017
1020
  candidate = cls._canonicalize_option_name(option)
@@ -1028,7 +1031,7 @@ class Git(metaclass=_GitMeta):
1028
1031
  raise UnsafeOptionError(
1029
1032
  f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
1030
1033
  )
1031
- if option_char not in clusterable_short_options:
1034
+ if option_char not in clusterable_short_options_set:
1032
1035
  break
1033
1036
  if not (option.startswith("--") or (options_are_kwargs and len(candidate) > 1)):
1034
1037
  continue
@@ -1133,7 +1136,7 @@ class Git(metaclass=_GitMeta):
1133
1136
  """List references in a remote repository.
1134
1137
 
1135
1138
  :param allow_unsafe_options:
1136
- Allow unsafe options, like ``--upload-pack``.
1139
+ Allow unsafe options, like ``--upload-pack`` or ``--exec``.
1137
1140
  """
1138
1141
  if not allow_unsafe_options:
1139
1142
  candidate_options = self._option_candidates(args, kwargs)
@@ -462,7 +462,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
462
462
  v = v[:-1]
463
463
  # END cut trailing escapes to prevent decode error
464
464
 
465
- return v.encode(defenc).decode("unicode_escape")
465
+ escapes = {"b": "\b", "n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"}
466
+ return re.sub(r"\\(.)", lambda match: escapes.get(match.group(1), match.group(0)), v)
466
467
 
467
468
  # END string_decode
468
469
 
@@ -517,10 +518,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
517
518
  # Opens quoting and does not close: appears to start multi-line quoting.
518
519
  is_multi_line = True
519
520
  optval = string_decode(optval[1:])
520
- elif optval.find("\\", 1, -1) == -1 and optval.find('"', 1, -1) == -1:
521
- # Opens and closes quoting. Single line, and all we need is quote removal.
522
- optval = optval[1:-1]
523
- # TODO: Handle other quoted content, especially well-formed backslash escapes.
521
+ elif re.search(r'(?:^|[^\\])(?:\\\\)*"', optval[1:-1]):
522
+ # Preserve malformed values containing unescaped quotes.
523
+ pass
524
+ else:
525
+ # Opens and closes quoting.
526
+ optval = string_decode(optval[1:-1])
524
527
 
525
528
  # Preserves multiple values for duplicate optnames.
526
529
  cursect.add(optname, optval)
@@ -705,7 +708,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
705
708
  continue
706
709
 
707
710
  for v in values:
708
- fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
711
+ value = self._value_to_string(v)
712
+ if any(char in value for char in '\n\t\b\\"#;') or value[:1].isspace() or value[-1:].isspace():
713
+ value = value.replace("\\", "\\\\").replace('"', '\\"')
714
+ value = '"%s\\\n"' % value.replace("\n", "\\n").replace("\t", "\\t").replace("\b", "\\b")
715
+ fp.write(("\t%s = %s\n" % (key, value)).encode(defenc))
709
716
  # END if key is not __name__
710
717
 
711
718
  # END section writing
@@ -764,6 +771,20 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
764
771
  return
765
772
  # END stop if we have include files
766
773
 
774
+ sections: List[_OMD] = [self._defaults]
775
+ section: _OMD
776
+ stored_section: _OMD
777
+ values: List[Any]
778
+ raw_value: Any
779
+ for _, stored_section in self._sections.items():
780
+ sections.append(stored_section)
781
+ for section in sections:
782
+ for key, values in section.items_all():
783
+ if key != "__name__":
784
+ for raw_value in values:
785
+ if "\r" in self._value_to_string(raw_value) or "\x00" in self._value_to_string(raw_value):
786
+ raise ValueError("Git config values must not contain CR or NUL")
787
+
767
788
  fp = self._file_or_files
768
789
 
769
790
  # We have a physical file on disk, so get a lock.
@@ -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:
@@ -95,14 +95,35 @@ This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
95
95
  :const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
96
96
  """
97
97
 
98
- _octal_byte_re = re.compile(rb"\\([0-9]{3})")
99
98
 
100
-
101
- def _octal_repl(matchobj: Match) -> bytes:
102
- value = matchobj.group(1)
103
- value = int(value, 8)
104
- value = bytes(bytearray((value,)))
105
- return value
99
+ def _unquote_path(path: bytes) -> bytes:
100
+ result = bytearray()
101
+ escapes = {
102
+ ord("a"): 7,
103
+ ord("b"): 8,
104
+ ord("f"): 12,
105
+ ord("n"): 10,
106
+ ord("r"): 13,
107
+ ord("t"): 9,
108
+ ord("v"): 11,
109
+ }
110
+ i = 0
111
+ while i < len(path):
112
+ if path[i] != ord("\\") or i + 1 == len(path):
113
+ result.append(path[i])
114
+ i += 1
115
+ continue
116
+ if path[i + 1] in b"0123" and i + 3 < len(path) and all(c in b"01234567" for c in path[i + 2 : i + 4]):
117
+ result.append(int(path[i + 1 : i + 4], 8))
118
+ i += 4
119
+ continue
120
+ escaped = path[i + 1]
121
+ if escaped in escapes or escaped in b'\\"':
122
+ result.append(escapes.get(escaped, escaped))
123
+ else:
124
+ result.extend(path[i : i + 2])
125
+ i += 2
126
+ return bytes(result)
106
127
 
107
128
 
108
129
  def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
@@ -110,9 +131,7 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
110
131
  return None
111
132
 
112
133
  if path.startswith(b'"') and path.endswith(b'"'):
113
- path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\")
114
-
115
- path = _octal_byte_re.sub(_octal_repl, path)
134
+ path = _unquote_path(path[1:-1])
116
135
 
117
136
  if has_ab_prefix:
118
137
  assert path.startswith(b"a/") or path.startswith(b"b/")
@@ -220,8 +239,8 @@ class Diffable:
220
239
  to be read and diffed.
221
240
 
222
241
  :param allow_unsafe_options:
223
- If ``True``, allow options such as ``--output`` that can write to arbitrary
224
- filesystem paths.
242
+ If ``True``, allow options such as ``--output``, ``--no-index``, and ``-O``
243
+ that can write to or read from arbitrary filesystem paths.
225
244
 
226
245
  :param kwargs:
227
246
  Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
@@ -238,7 +257,8 @@ class Diffable:
238
257
  if not allow_unsafe_options:
239
258
  Git.check_unsafe_options(
240
259
  options=Git._option_candidates([other], kwargs),
241
- unsafe_options=self.repo.unsafe_git_revision_options,
260
+ unsafe_options=self.repo.unsafe_git_diff_options,
261
+ clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
242
262
  )
243
263
 
244
264
  args: List[Union[PathLike, Diffable]] = []
@@ -1567,7 +1567,8 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1567
1567
  if not allow_unsafe_options:
1568
1568
  Git.check_unsafe_options(
1569
1569
  options=Git._option_candidates([other], kwargs),
1570
- unsafe_options=self.repo.unsafe_git_revision_options,
1570
+ unsafe_options=self.repo.unsafe_git_diff_options,
1571
+ clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
1571
1572
  )
1572
1573
 
1573
1574
  # Only run if we are the default repository index.
@@ -9,6 +9,7 @@ import logging
9
9
  import ntpath
10
10
  import os
11
11
  import os.path as osp
12
+ import shlex
12
13
  import stat
13
14
  import sys
14
15
  import uuid
@@ -270,7 +271,7 @@ class Submodule(IndexObject, TraversableIterableObj):
270
271
  raise ValueError("Cannot write blobs of 'historical' submodule configurations")
271
272
  # END handle writes of historical submodules
272
273
 
273
- return SubmoduleConfigParser(fp_module, read_only=read_only)
274
+ return SubmoduleConfigParser(fp_module, read_only=read_only, merge_includes=False)
274
275
 
275
276
  def _clear_cache(self) -> None:
276
277
  """Clear the possibly changed values."""
@@ -363,6 +364,15 @@ class Submodule(IndexObject, TraversableIterableObj):
363
364
  module_abspath = cls._module_abspath(repo, path, name)
364
365
  module_checkout_path = module_abspath
365
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
366
376
  kwargs["separate_git_dir"] = module_abspath
367
377
  module_abspath_dir = osp.dirname(module_abspath)
368
378
  if not osp.isdir(module_abspath_dir):
@@ -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"]
@@ -45,7 +45,6 @@ from git.util import (
45
45
 
46
46
  from .fun import (
47
47
  find_submodule_git_dir,
48
- find_worktree_git_dir,
49
48
  is_git_dir,
50
49
  rev_parse,
51
50
  touch,
@@ -159,6 +158,8 @@ class Repo:
159
158
  "-c",
160
159
  # Can install hooks that execute during clone:
161
160
  "--template",
161
+ # Redirects the repository metadata to a caller-controlled path:
162
+ "--separate-git-dir",
162
163
  # Fetches from an additional caller-controlled URI:
163
164
  "--bundle-uri",
164
165
  ]
@@ -199,6 +200,21 @@ class Repo:
199
200
  "-o",
200
201
  ]
201
202
 
203
+ unsafe_git_blame_options = unsafe_git_revision_options + [
204
+ # These options read from arbitrary files and expose their contents through blame output.
205
+ "--contents",
206
+ "-S",
207
+ "--ignore-revs-file",
208
+ ]
209
+
210
+ unsafe_git_diff_options = unsafe_git_revision_options + [
211
+ # Treats path operands as arbitrary filesystem paths.
212
+ "--no-index",
213
+ # Reads caller-controlled order patterns from an arbitrary file.
214
+ "-O",
215
+ "--orderfile",
216
+ ]
217
+
202
218
  # Invariants
203
219
  config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
204
220
  """Represents the configuration level of a configuration file."""
@@ -217,6 +233,10 @@ class Repo:
217
233
  ) -> None:
218
234
  R"""Create a new :class:`Repo` instance.
219
235
 
236
+ .. note::
237
+ Repositories using reftable may be opened, but GitPython's direct reference
238
+ access does not support reftable.
239
+
220
240
  :param path:
221
241
  The path to either the worktree directory or the .git directory itself::
222
242
 
@@ -251,7 +271,11 @@ class Repo:
251
271
  :class:`Repo`
252
272
  """
253
273
 
254
- epath = path or os.getenv("GIT_DIR")
274
+ git_dir_env = os.getenv("GIT_DIR")
275
+ object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY")
276
+ if object_dir_env is not None:
277
+ object_dir_env = osp.abspath(object_dir_env)
278
+ epath = path or git_dir_env
255
279
  if not epath:
256
280
  epath = os.getcwd()
257
281
  epath = os.fspath(epath)
@@ -273,37 +297,48 @@ class Repo:
273
297
  raise NoSuchPathError(epath)
274
298
 
275
299
  # Walk up the path to find the `.git` dir.
276
- curpath = epath
277
- git_dir = None
300
+ curpath = os.fspath(epath) if epath is not None else ""
301
+ git_dir: Optional[str] = None
302
+ explicit_git_dir = not path and bool(git_dir_env)
278
303
  while curpath:
279
304
  # ABOUT osp.NORMPATH
280
305
  # It's important to normalize the paths, as submodules will otherwise
281
306
  # initialize their repo instances with paths that depend on path-portions
282
307
  # that will not exist after being removed. It's just cleaner.
283
- if (
284
- osp.isfile(osp.join(curpath, "gitdir"))
285
- and osp.isfile(osp.join(curpath, "commondir"))
286
- and osp.isfile(osp.join(curpath, "HEAD"))
287
- ):
288
- git_dir = curpath
308
+ if not explicit_git_dir:
309
+ dotgit = osp.join(curpath, ".git")
310
+ try:
311
+ sm_gitpath = find_submodule_git_dir(dotgit)
312
+ except OSError:
313
+ break
314
+ if sm_gitpath is not None:
315
+ # Worktrees can use relative paths as of Git 2.48, so join to curpath.
316
+ git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
317
+ self._working_tree_dir = curpath
318
+ break
319
+
320
+ # Like Git, do not fall back to a bare repository or parent directory when
321
+ # a non-directory .git entry exists but is not a valid gitfile.
322
+ if osp.exists(dotgit) and not osp.isdir(dotgit):
323
+ break
289
324
 
290
- if "GIT_WORK_TREE" in os.environ:
291
- self._working_tree_dir = os.getenv("GIT_WORK_TREE")
292
- else:
293
- # Linked worktree administrative directories store the path to the
294
- # worktree's .git file in their gitdir file (without "gitdir: " prefix).
295
- with open(osp.join(git_dir, "gitdir")) as fp:
296
- worktree_gitfile = fp.read().strip()
325
+ if is_git_dir(curpath):
326
+ git_dir = curpath
327
+ if osp.isfile(osp.join(curpath, "gitdir")) and osp.isfile(osp.join(curpath, "commondir")):
328
+ if "GIT_WORK_TREE" in os.environ:
329
+ self._working_tree_dir = os.getenv("GIT_WORK_TREE")
330
+ else:
331
+ # Linked worktree administrative directories store the path to
332
+ # the worktree's .git file in gitdir (without a "gitdir: " prefix).
333
+ with open(osp.join(git_dir, "gitdir")) as fp:
334
+ worktree_gitfile = fp.read().strip()
297
335
 
298
- if not osp.isabs(worktree_gitfile):
299
- worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
336
+ if not osp.isabs(worktree_gitfile):
337
+ worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
300
338
 
301
- self._working_tree_dir = osp.dirname(worktree_gitfile)
339
+ self._working_tree_dir = osp.dirname(worktree_gitfile)
340
+ break
302
341
 
303
- break
304
-
305
- if is_git_dir(curpath):
306
- git_dir = curpath
307
342
  # from man git-config : core.worktree
308
343
  # Set the path to the root of the working tree. If GIT_COMMON_DIR
309
344
  # environment variable is set, core.worktree is ignored and not used for
@@ -323,22 +358,7 @@ class Repo:
323
358
  self._working_tree_dir = os.getenv("GIT_WORK_TREE")
324
359
  break
325
360
 
326
- dotgit = osp.join(curpath, ".git")
327
- sm_gitpath = find_submodule_git_dir(dotgit)
328
- if sm_gitpath is not None:
329
- git_dir = osp.normpath(sm_gitpath)
330
-
331
- sm_gitpath = find_submodule_git_dir(dotgit)
332
- if sm_gitpath is None:
333
- sm_gitpath = find_worktree_git_dir(dotgit)
334
-
335
- if sm_gitpath is not None:
336
- # worktrees can use relative paths as of Git 2.48, so we join to curpath
337
- git_dir = osp.normpath(osp.join(curpath, sm_gitpath))
338
- self._working_tree_dir = curpath
339
- break
340
-
341
- if not search_parent_directories:
361
+ if explicit_git_dir or not search_parent_directories:
342
362
  break
343
363
  curpath, tail = osp.split(curpath)
344
364
  if not tail:
@@ -349,6 +369,16 @@ class Repo:
349
369
  raise InvalidGitRepositoryError(epath)
350
370
  self.git_dir = git_dir
351
371
 
372
+ common_dir_env = os.getenv("GIT_COMMON_DIR")
373
+ if common_dir_env is not None:
374
+ self._common_dir = osp.abspath(common_dir_env)
375
+ else:
376
+ try:
377
+ common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n")
378
+ self._common_dir = osp.join(self.git_dir, common_dir)
379
+ except OSError:
380
+ self._common_dir = ""
381
+
352
382
  self._bare = False
353
383
  try:
354
384
  self._bare = self.config_reader("repository").getboolean("core", "bare")
@@ -356,12 +386,6 @@ class Repo:
356
386
  # Let's not assume the option exists, although it should.
357
387
  pass
358
388
 
359
- try:
360
- common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
361
- self._common_dir = osp.join(self.git_dir, common_dir)
362
- except OSError:
363
- self._common_dir = ""
364
-
365
389
  # Adjust the working directory in case we are actually bare - we didn't know
366
390
  # that in the first place.
367
391
  if self._bare:
@@ -370,9 +394,15 @@ class Repo:
370
394
 
371
395
  self.working_dir: PathLike = self._working_tree_dir or self.common_dir
372
396
  self.git = self.GitCommandWrapperType(self.working_dir)
397
+ if common_dir_env is not None:
398
+ self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir))
399
+ elif git_dir_env is not None:
400
+ self.git.update_environment(GIT_DIR=os.fspath(self.git_dir))
401
+ if object_dir_env is not None:
402
+ self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env)
373
403
 
374
404
  # Special handling, in special times.
375
- rootpath = osp.join(self.common_dir, "objects")
405
+ rootpath = object_dir_env if object_dir_env is not None else osp.join(self.common_dir, "objects")
376
406
  if issubclass(odbt, GitCmdObjectDB):
377
407
  self.odb = odbt(rootpath, self.git)
378
408
  else:
@@ -973,7 +1003,7 @@ class Repo:
973
1003
  :return:
974
1004
  List of strings being pathnames of alternates
975
1005
  """
976
- alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1006
+ alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
977
1007
 
978
1008
  if osp.exists(alternates_path):
979
1009
  with open(alternates_path, "rb") as f:
@@ -994,7 +1024,7 @@ class Repo:
994
1024
  The method does not check for the existence of the paths in `alts`, as the
995
1025
  caller is responsible.
996
1026
  """
997
- alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1027
+ alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
998
1028
  if not alts:
999
1029
  if osp.isfile(alternates_path):
1000
1030
  os.remove(alternates_path)
@@ -1149,7 +1179,7 @@ class Repo:
1149
1179
  :manpage:`git-rev-parse(1)` is a valid option.
1150
1180
 
1151
1181
  :param allow_unsafe_options:
1152
- Allow unsafe options in revision argument, like ``--output``.
1182
+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1153
1183
 
1154
1184
  :return:
1155
1185
  Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
@@ -1161,7 +1191,9 @@ class Repo:
1161
1191
  """
1162
1192
  if not allow_unsafe_options:
1163
1193
  Git.check_unsafe_options(
1164
- options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options
1194
+ options=Git._option_candidates([rev], kwargs),
1195
+ unsafe_options=self.unsafe_git_blame_options,
1196
+ clusterable_short_options="46bceflnpqstvw",
1165
1197
  )
1166
1198
 
1167
1199
  data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
@@ -1253,7 +1285,7 @@ class Repo:
1253
1285
  :manpage:`git-rev-parse(1)` is a valid option.
1254
1286
 
1255
1287
  :param allow_unsafe_options:
1256
- Allow unsafe options in revision argument, like ``--output``.
1288
+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1257
1289
 
1258
1290
  :return:
1259
1291
  list: [git.Commit, list: [<line>]]
@@ -1269,7 +1301,8 @@ class Repo:
1269
1301
  if not allow_unsafe_options:
1270
1302
  Git.check_unsafe_options(
1271
1303
  options=Git._option_candidates([rev, rev_opts_list], kwargs),
1272
- unsafe_options=self.unsafe_git_revision_options,
1304
+ unsafe_options=self.unsafe_git_blame_options,
1305
+ clusterable_short_options="46bceflnpqstvw",
1273
1306
  )
1274
1307
  data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs)
1275
1308
  commits: Dict[str, Commit] = {}
@@ -60,22 +60,61 @@ def touch(filename: str) -> str:
60
60
  def is_git_dir(d: PathLike) -> bool:
61
61
  """This is taken from the git setup.c:is_git_directory function.
62
62
 
63
+ .. note::
64
+ This function recognizes repositories using reftable through their
65
+ compatibility files, but GitPython's direct reference access does not support
66
+ reftable.
67
+
63
68
  :raise git.exc.WorkTreeRepositoryUnsupported:
64
69
  If it sees a worktree directory. It's quite hacky to do that here, but at least
65
70
  clearly indicates that we don't support it. There is the unlikely danger to
66
71
  throw if we see directories which just look like a worktree dir, but are none.
67
72
  """
68
73
  if osp.isdir(d):
69
- if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
70
- osp.join(d, "refs")
71
- ):
72
- headref = osp.join(d, "HEAD")
73
- return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
74
- elif (
75
- osp.isfile(osp.join(d, "gitdir"))
76
- and osp.isfile(osp.join(d, "commondir"))
77
- and osp.isfile(osp.join(d, "gitfile"))
78
- ):
74
+ headref = osp.join(d, "HEAD")
75
+ if osp.islink(headref):
76
+ try:
77
+ valid_head = os.readlink(headref).startswith("refs/")
78
+ except OSError:
79
+ valid_head = False
80
+ else:
81
+ try:
82
+ with open(headref, "rb") as fp:
83
+ head = fp.read(256)
84
+ except OSError:
85
+ valid_head = False
86
+ else:
87
+ valid_head = (head.startswith(b"ref:") and head[4:].lstrip().startswith(b"refs/")) or bool(
88
+ re.match(rb"(?:[0-9A-Fa-f]{64}|[0-9A-Fa-f]{40})", head)
89
+ )
90
+
91
+ common_dir = os.getenv("GIT_COMMON_DIR")
92
+ if common_dir == "":
93
+ return False
94
+ if common_dir is None:
95
+ common_dir_file = Path(d) / "commondir"
96
+ try:
97
+ common_dir = os.fsdecode(common_dir_file.read_bytes()).rstrip("\r\n")
98
+ except FileNotFoundError:
99
+ if osp.lexists(common_dir_file):
100
+ return False
101
+ common_dir = os.fspath(d)
102
+ except (OSError, UnicodeError):
103
+ return False
104
+ else:
105
+ if not common_dir:
106
+ return False
107
+ try:
108
+ common_dir = osp.realpath(osp.join(d, common_dir))
109
+ except (OSError, ValueError):
110
+ return False
111
+
112
+ object_dir = os.getenv("GIT_OBJECT_DIRECTORY")
113
+ if object_dir is None:
114
+ object_dir = osp.join(common_dir, "objects")
115
+ if valid_head and osp.isdir(object_dir) and osp.isdir(osp.join(common_dir, "refs")):
116
+ return True
117
+ if osp.isfile(osp.join(d, "gitdir")) and osp.isfile(osp.join(d, "commondir")) and osp.isfile(headref):
79
118
  raise WorkTreeRepositoryUnsupported(d)
80
119
  return False
81
120
 
@@ -84,19 +123,20 @@ def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
84
123
  """Search for a gitdir for this worktree."""
85
124
  try:
86
125
  statbuf = os.stat(dotgit)
87
- except OSError:
126
+ except (FileNotFoundError, NotADirectoryError):
88
127
  return None
89
- if not stat.S_ISREG(statbuf.st_mode):
128
+ if not stat.S_ISREG(statbuf.st_mode) or statbuf.st_size > (1 << 20):
90
129
  return None
91
130
 
92
131
  try:
93
- lines = Path(dotgit).read_text().splitlines()
94
- for key, value in [line.strip().split(": ") for line in lines]:
95
- if key == "gitdir":
96
- return value
97
- except ValueError:
98
- pass
99
- return None
132
+ with open(dotgit, "rb") as fp:
133
+ content_bytes = fp.read(statbuf.st_size)
134
+ if len(content_bytes) != statbuf.st_size:
135
+ return None
136
+ content = os.fsdecode(content_bytes).rstrip("\r\n")
137
+ except (OSError, UnicodeError):
138
+ return None
139
+ return content[8:] if len(content) >= 9 and content.startswith("gitdir: ") else None
100
140
 
101
141
 
102
142
  def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
@@ -104,26 +144,18 @@ def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
104
144
  if is_git_dir(d):
105
145
  return d
106
146
 
107
- try:
108
- with open(d) as fp:
109
- content = fp.read().rstrip()
110
- except IOError:
111
- # It's probably not a file.
112
- pass
113
- else:
114
- if content.startswith("gitdir: "):
115
- path = content[8:]
116
-
117
- if Git.is_cygwin():
118
- # Cygwin creates submodules prefixed with `/cygdrive/...`.
119
- # Cygwin git understands Cygwin paths much better than Windows ones.
120
- # Also the Cygwin tests are assuming Cygwin paths.
121
- path = cygpath(path)
122
- if not osp.isabs(path):
123
- path = osp.normpath(osp.join(osp.dirname(d), path))
124
- return find_submodule_git_dir(path)
125
- # END handle exception
126
- return None
147
+ path = find_worktree_git_dir(d)
148
+ if path is None:
149
+ return None
150
+
151
+ if Git.is_cygwin():
152
+ # Cygwin creates submodules prefixed with `/cygdrive/...`.
153
+ # Cygwin git understands Cygwin paths much better than Windows ones.
154
+ # Also the Cygwin tests are assuming Cygwin paths.
155
+ path = cygpath(path)
156
+ if not osp.isabs(path):
157
+ path = osp.normpath(osp.join(osp.dirname(d), path))
158
+ return path if is_git_dir(path) else None
127
159
 
128
160
 
129
161
  def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
@@ -858,10 +858,6 @@ class Actor:
858
858
  committers and authors or anything with a name and an email as mentioned in the git
859
859
  log entries."""
860
860
 
861
- # PRECOMPILED REGEX
862
- name_only_regex = re.compile(r"<(.*)>")
863
- name_email_regex = re.compile(r"(.*) <(.*?)>")
864
-
865
861
  # ENVIRONMENT VARIABLES
866
862
  # These are read when creating new commits.
867
863
  env_author_name = "GIT_AUTHOR_NAME"
@@ -906,18 +902,14 @@ class Actor:
906
902
  :return:
907
903
  :class:`Actor`
908
904
  """
909
- m = cls.name_email_regex.search(string)
910
- if m:
911
- name, email = m.groups()
912
- return Actor(name, email)
913
- else:
914
- m = cls.name_only_regex.search(string)
915
- if m:
916
- return Actor(m.group(1), None)
917
- # Assume the best and use the whole string as name.
918
- return Actor(string, None)
919
- # END special case name
920
- # END handle name/email matching
905
+ line = string.partition("\n")[0]
906
+ left_bracket = line.find("<")
907
+ right_bracket = line.find(">", left_bracket + 1)
908
+ if left_bracket >= 0 and right_bracket >= 0:
909
+ return Actor(line[:left_bracket].rstrip(), line[left_bracket + 1 : right_bracket])
910
+
911
+ # Assume the best and use the whole string as name.
912
+ return Actor(string, None)
921
913
 
922
914
  @classmethod
923
915
  def _main_actor(
gitpython-3.1.58/VERSION DELETED
@@ -1 +0,0 @@
1
- 3.1.58
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