fuuuzzy commited on
Commit
2d38e41
·
verified ·
1 Parent(s): 2dfbce6

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +18 -0
  2. .venv/bin/ruff +3 -0
  3. .venv/lib/python3.11/site-packages/dill/__pycache__/_dill.cpython-311.pyc +3 -0
  4. .venv/lib/python3.11/site-packages/git/__init__.py +300 -0
  5. .venv/lib/python3.11/site-packages/git/cmd.py +1741 -0
  6. .venv/lib/python3.11/site-packages/git/compat.py +165 -0
  7. .venv/lib/python3.11/site-packages/git/config.py +951 -0
  8. .venv/lib/python3.11/site-packages/git/db.py +71 -0
  9. .venv/lib/python3.11/site-packages/git/diff.py +775 -0
  10. .venv/lib/python3.11/site-packages/git/exc.py +228 -0
  11. .venv/lib/python3.11/site-packages/git/index/__init__.py +16 -0
  12. .venv/lib/python3.11/site-packages/git/index/base.py +1532 -0
  13. .venv/lib/python3.11/site-packages/git/index/fun.py +465 -0
  14. .venv/lib/python3.11/site-packages/git/index/typ.py +202 -0
  15. .venv/lib/python3.11/site-packages/git/index/util.py +121 -0
  16. .venv/lib/python3.11/site-packages/git/objects/__init__.py +25 -0
  17. .venv/lib/python3.11/site-packages/git/objects/base.py +301 -0
  18. .venv/lib/python3.11/site-packages/git/objects/blob.py +48 -0
  19. .venv/lib/python3.11/site-packages/git/objects/commit.py +909 -0
  20. .venv/lib/python3.11/site-packages/git/objects/fun.py +281 -0
  21. .venv/lib/python3.11/site-packages/git/objects/submodule/__init__.py +7 -0
  22. .venv/lib/python3.11/site-packages/git/objects/submodule/base.py +1642 -0
  23. .venv/lib/python3.11/site-packages/git/objects/submodule/root.py +467 -0
  24. .venv/lib/python3.11/site-packages/git/objects/submodule/util.py +121 -0
  25. .venv/lib/python3.11/site-packages/git/objects/tag.py +140 -0
  26. .venv/lib/python3.11/site-packages/git/objects/tree.py +416 -0
  27. .venv/lib/python3.11/site-packages/git/objects/util.py +700 -0
  28. .venv/lib/python3.11/site-packages/git/py.typed +0 -0
  29. .venv/lib/python3.11/site-packages/git/refs/__init__.py +21 -0
  30. .venv/lib/python3.11/site-packages/git/refs/head.py +304 -0
  31. .venv/lib/python3.11/site-packages/git/refs/log.py +399 -0
  32. .venv/lib/python3.11/site-packages/git/refs/reference.py +176 -0
  33. .venv/lib/python3.11/site-packages/git/refs/remote.py +79 -0
  34. .venv/lib/python3.11/site-packages/git/refs/symbolic.py +933 -0
  35. .venv/lib/python3.11/site-packages/git/refs/tag.py +155 -0
  36. .venv/lib/python3.11/site-packages/git/remote.py +1248 -0
  37. .venv/lib/python3.11/site-packages/git/repo/__init__.py +8 -0
  38. .venv/lib/python3.11/site-packages/git/repo/base.py +1638 -0
  39. .venv/lib/python3.11/site-packages/git/repo/fun.py +425 -0
  40. .venv/lib/python3.11/site-packages/git/types.py +285 -0
  41. .venv/lib/python3.11/site-packages/git/util.py +1350 -0
  42. .venv/lib/python3.11/site-packages/google/_upb/_message.abi3.so +3 -0
  43. .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/blob.cpython-311.pyc +3 -0
  44. .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/bucket.cpython-311.pyc +3 -0
  45. .venv/lib/python3.11/site-packages/huggingface_hub/__init__.py +1554 -0
  46. .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/__init__.cpython-311.pyc +0 -0
  47. .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_commit_api.cpython-311.pyc +0 -0
  48. .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_inference_endpoints.cpython-311.pyc +0 -0
  49. .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_jobs_api.cpython-311.pyc +0 -0
  50. .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_local_folder.cpython-311.pyc +0 -0
.gitattributes CHANGED
@@ -46,3 +46,21 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
46
  .venv/lib/python3.11/site-packages/nvidia/cuda_cupti/lib/libpcsamplingutil.so filter=lfs diff=lfs merge=lfs -text
47
  .venv/lib/python3.11/site-packages/nvidia/cublas/lib/libnvblas.so.12 filter=lfs diff=lfs merge=lfs -text
48
  .venv/lib/python3.11/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc-builtins.so.12.8 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  .venv/lib/python3.11/site-packages/nvidia/cuda_cupti/lib/libpcsamplingutil.so filter=lfs diff=lfs merge=lfs -text
47
  .venv/lib/python3.11/site-packages/nvidia/cublas/lib/libnvblas.so.12 filter=lfs diff=lfs merge=lfs -text
48
  .venv/lib/python3.11/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc-builtins.so.12.8 filter=lfs diff=lfs merge=lfs -text
49
+ .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/blob.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
50
+ .venv/lib/python3.11/site-packages/dill/__pycache__/_dill.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
51
+ .venv/bin/ruff filter=lfs diff=lfs merge=lfs -text
52
+ .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/bucket.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
53
+ .venv/lib/python3.11/site-packages/nvidia/nvshmem/lib/libnvshmem_device.a filter=lfs diff=lfs merge=lfs -text
54
+ .venv/lib/python3.11/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc-builtins.alt.so.12.8 filter=lfs diff=lfs merge=lfs -text
55
+ .venv/lib/python3.11/site-packages/google/_upb/_message.abi3.so filter=lfs diff=lfs merge=lfs -text
56
+ .venv/lib/python3.11/site-packages/nvidia/cudnn/lib/libcudnn.so.9 filter=lfs diff=lfs merge=lfs -text
57
+ .venv/lib/python3.11/site-packages/msgpack/_cmsgpack.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
58
+ .venv/lib/python3.11/site-packages/nvidia/cufft/lib/libcufftw.so.11 filter=lfs diff=lfs merge=lfs -text
59
+ .venv/lib/python3.11/site-packages/nvidia/cuda_cupti/lib/libcheckpoint.so filter=lfs diff=lfs merge=lfs -text
60
+ .venv/lib/python3.11/site-packages/nvidia/cudnn/lib/libcudnn_cnn.so.9 filter=lfs diff=lfs merge=lfs -text
61
+ .venv/lib/python3.11/site-packages/nvidia/nvjitlink/lib/libnvJitLink.so.12 filter=lfs diff=lfs merge=lfs -text
62
+ .venv/lib/python3.11/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.alt.so.12 filter=lfs diff=lfs merge=lfs -text
63
+ .venv/lib/python3.11/site-packages/nvidia/cudnn/lib/libcudnn_engines_runtime_compiled.so.9 filter=lfs diff=lfs merge=lfs -text
64
+ .venv/lib/python3.11/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so.12 filter=lfs diff=lfs merge=lfs -text
65
+ .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/hf_api.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
66
+ .venv/lib/python3.11/site-packages/nvidia/cudnn/lib/libcudnn_graph.so.9 filter=lfs diff=lfs merge=lfs -text
.venv/bin/ruff ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1497dfe275c08dfa91e07e488b28dd6d0036db5d3ef82c8a26ed385049c25943
3
+ size 35528032
.venv/lib/python3.11/site-packages/dill/__pycache__/_dill.cpython-311.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f88f85aa4bca2d752b3be88cf4cc5fed1e7c6be0f77d9d77dd56c603c97733aa
3
+ size 110272
.venv/lib/python3.11/site-packages/git/__init__.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ # @PydevCodeAnalysisIgnore
7
+
8
+ __all__ = [
9
+ "Actor",
10
+ "AmbiguousObjectName",
11
+ "BadName",
12
+ "BadObject",
13
+ "BadObjectType",
14
+ "BaseIndexEntry",
15
+ "Blob",
16
+ "BlobFilter",
17
+ "BlockingLockFile",
18
+ "CacheError",
19
+ "CheckoutError",
20
+ "CommandError",
21
+ "Commit",
22
+ "Diff",
23
+ "DiffConstants",
24
+ "DiffIndex",
25
+ "Diffable",
26
+ "FetchInfo",
27
+ "Git",
28
+ "GitCmdObjectDB",
29
+ "GitCommandError",
30
+ "GitCommandNotFound",
31
+ "GitConfigParser",
32
+ "GitDB",
33
+ "GitError",
34
+ "HEAD",
35
+ "Head",
36
+ "HookExecutionError",
37
+ "INDEX",
38
+ "IndexEntry",
39
+ "IndexFile",
40
+ "IndexObject",
41
+ "InvalidDBRoot",
42
+ "InvalidGitRepositoryError",
43
+ "List", # Deprecated - import this from `typing` instead.
44
+ "LockFile",
45
+ "NULL_TREE",
46
+ "NoSuchPathError",
47
+ "ODBError",
48
+ "Object",
49
+ "Optional", # Deprecated - import this from `typing` instead.
50
+ "ParseError",
51
+ "PathLike",
52
+ "PushInfo",
53
+ "RefLog",
54
+ "RefLogEntry",
55
+ "Reference",
56
+ "Remote",
57
+ "RemoteProgress",
58
+ "RemoteReference",
59
+ "Repo",
60
+ "RepositoryDirtyError",
61
+ "RootModule",
62
+ "RootUpdateProgress",
63
+ "Sequence", # Deprecated - import from `typing`, or `collections.abc` in 3.9+.
64
+ "StageType",
65
+ "Stats",
66
+ "Submodule",
67
+ "SymbolicReference",
68
+ "TYPE_CHECKING", # Deprecated - import this from `typing` instead.
69
+ "Tag",
70
+ "TagObject",
71
+ "TagReference",
72
+ "Tree",
73
+ "TreeModifier",
74
+ "Tuple", # Deprecated - import this from `typing` instead.
75
+ "Union", # Deprecated - import this from `typing` instead.
76
+ "UnmergedEntriesError",
77
+ "UnsafeOptionError",
78
+ "UnsafeProtocolError",
79
+ "UnsupportedOperation",
80
+ "UpdateProgress",
81
+ "WorkTreeRepositoryUnsupported",
82
+ "refresh",
83
+ "remove_password_if_present",
84
+ "rmtree",
85
+ "safe_decode",
86
+ "to_hex_sha",
87
+ ]
88
+
89
+ __version__ = '3.1.45'
90
+
91
+ from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
+
93
+ if TYPE_CHECKING:
94
+ from types import ModuleType
95
+
96
+ import warnings
97
+
98
+ from gitdb.util import to_hex_sha
99
+
100
+ from git.exc import (
101
+ AmbiguousObjectName,
102
+ BadName,
103
+ BadObject,
104
+ BadObjectType,
105
+ CacheError,
106
+ CheckoutError,
107
+ CommandError,
108
+ GitCommandError,
109
+ GitCommandNotFound,
110
+ GitError,
111
+ HookExecutionError,
112
+ InvalidDBRoot,
113
+ InvalidGitRepositoryError,
114
+ NoSuchPathError,
115
+ ODBError,
116
+ ParseError,
117
+ RepositoryDirtyError,
118
+ UnmergedEntriesError,
119
+ UnsafeOptionError,
120
+ UnsafeProtocolError,
121
+ UnsupportedOperation,
122
+ WorkTreeRepositoryUnsupported,
123
+ )
124
+ from git.types import PathLike
125
+
126
+ try:
127
+ from git.compat import safe_decode # @NoMove
128
+ from git.config import GitConfigParser # @NoMove
129
+ from git.objects import ( # @NoMove
130
+ Blob,
131
+ Commit,
132
+ IndexObject,
133
+ Object,
134
+ RootModule,
135
+ RootUpdateProgress,
136
+ Submodule,
137
+ TagObject,
138
+ Tree,
139
+ TreeModifier,
140
+ UpdateProgress,
141
+ )
142
+ from git.refs import ( # @NoMove
143
+ HEAD,
144
+ Head,
145
+ RefLog,
146
+ RefLogEntry,
147
+ Reference,
148
+ RemoteReference,
149
+ SymbolicReference,
150
+ Tag,
151
+ TagReference,
152
+ )
153
+ from git.diff import ( # @NoMove
154
+ INDEX,
155
+ NULL_TREE,
156
+ Diff,
157
+ DiffConstants,
158
+ DiffIndex,
159
+ Diffable,
160
+ )
161
+ from git.db import GitCmdObjectDB, GitDB # @NoMove
162
+ from git.cmd import Git # @NoMove
163
+ from git.repo import Repo # @NoMove
164
+ from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove
165
+ from git.index import ( # @NoMove
166
+ BaseIndexEntry,
167
+ BlobFilter,
168
+ CheckoutError,
169
+ IndexEntry,
170
+ IndexFile,
171
+ StageType,
172
+ # NOTE: This tells type checkers what util resolves to. We delete it, and it is
173
+ # really resolved by __getattr__, which warns. See below on what to use instead.
174
+ util,
175
+ )
176
+ from git.util import ( # @NoMove
177
+ Actor,
178
+ BlockingLockFile,
179
+ LockFile,
180
+ Stats,
181
+ remove_password_if_present,
182
+ rmtree,
183
+ )
184
+ except GitError as _exc:
185
+ raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
186
+
187
+
188
+ def _warned_import(message: str, fullname: str) -> "ModuleType":
189
+ import importlib
190
+
191
+ warnings.warn(message, DeprecationWarning, stacklevel=3)
192
+ return importlib.import_module(fullname)
193
+
194
+
195
+ def _getattr(name: str) -> Any:
196
+ # TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
197
+
198
+ if name == "util":
199
+ return _warned_import(
200
+ "The expression `git.util` and the import `from git import util` actually "
201
+ "reference git.index.util, and not the git.util module accessed in "
202
+ '`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
203
+ "confusing behavior is currently preserved for compatibility, but may be "
204
+ "changed in the future and should not be relied on.",
205
+ fullname="git.index.util",
206
+ )
207
+
208
+ for names, prefix in (
209
+ ({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
210
+ ({"base", "fun", "typ"}, "git.index"),
211
+ ):
212
+ if name not in names:
213
+ continue
214
+
215
+ fullname = f"{prefix}.{name}"
216
+
217
+ return _warned_import(
218
+ f"{__name__}.{name} is a private alias of {fullname} and subject to "
219
+ f"immediate removal. Use {fullname} instead.",
220
+ fullname=fullname,
221
+ )
222
+
223
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
224
+
225
+
226
+ if not TYPE_CHECKING:
227
+ # NOTE: The expression `git.util` gives git.index.util and `from git import util`
228
+ # imports git.index.util, NOT git.util. It may not be feasible to change this until
229
+ # the next major version, to avoid breaking code inadvertently relying on it.
230
+ #
231
+ # - If git.index.util *is* what you want, use (or import from) that, to avoid
232
+ # confusion.
233
+ #
234
+ # - To use the "real" git.util module, write `from git.util import ...`, or if
235
+ # necessary access it as `sys.modules["git.util"]`.
236
+ #
237
+ # Note also that `import git.util` technically imports the "real" git.util... but
238
+ # the *expression* `git.util` after doing so is still git.index.util!
239
+ #
240
+ # (This situation differs from that of other indirect-submodule imports that are
241
+ # unambiguously non-public and subject to immediate removal. Here, the public
242
+ # git.util module, though different, makes less discoverable that the expression
243
+ # `git.util` refers to a non-public attribute of the git module.)
244
+ #
245
+ # This had originally come about by a wildcard import. Now that all intended imports
246
+ # are explicit, the intuitive but potentially incompatible binding occurs due to the
247
+ # usual rules for Python submodule bindings. So for now we replace that binding with
248
+ # git.index.util, delete that, and let __getattr__ handle it and issue a warning.
249
+ #
250
+ # For the same runtime behavior, it would be enough to forgo importing util, and
251
+ # delete util as created naturally; __getattr__ would behave the same. But type
252
+ # checkers would not know what util refers to when accessed as an attribute of git.
253
+ del util
254
+
255
+ # This is "hidden" to preserve static checking for undefined/misspelled attributes.
256
+ __getattr__ = _getattr
257
+
258
+ # { Initialize git executable path
259
+
260
+ GIT_OK = None
261
+
262
+
263
+ def refresh(path: Optional[PathLike] = None) -> None:
264
+ """Convenience method for setting the git executable path.
265
+
266
+ :param path:
267
+ Optional path to the Git executable. If not absolute, it is resolved
268
+ immediately, relative to the current directory.
269
+
270
+ :note:
271
+ The `path` parameter is usually omitted and cannot be used to specify a custom
272
+ command whose location is looked up in a path search on each call. See
273
+ :meth:`Git.refresh <git.cmd.Git.refresh>` for details on how to achieve this.
274
+
275
+ :note:
276
+ This calls :meth:`Git.refresh <git.cmd.Git.refresh>` and sets other global
277
+ configuration according to the effect of doing so. As such, this function should
278
+ usually be used instead of using :meth:`Git.refresh <git.cmd.Git.refresh>` or
279
+ :meth:`FetchInfo.refresh <git.remote.FetchInfo.refresh>` directly.
280
+
281
+ :note:
282
+ This function is called automatically, with no arguments, at import time.
283
+ """
284
+ global GIT_OK
285
+ GIT_OK = False
286
+
287
+ if not Git.refresh(path=path):
288
+ return
289
+ if not FetchInfo.refresh(): # noqa: F405
290
+ return # type: ignore[unreachable]
291
+
292
+ GIT_OK = True
293
+
294
+
295
+ try:
296
+ refresh()
297
+ except Exception as _exc:
298
+ raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc
299
+
300
+ # } END initialize git executable path
.venv/lib/python3.11/site-packages/git/cmd.py ADDED
@@ -0,0 +1,1741 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ from __future__ import annotations
7
+
8
+ __all__ = ["GitMeta", "Git"]
9
+
10
+ import contextlib
11
+ import io
12
+ import itertools
13
+ import logging
14
+ import os
15
+ import re
16
+ import signal
17
+ import subprocess
18
+ from subprocess import DEVNULL, PIPE, Popen
19
+ import sys
20
+ from textwrap import dedent
21
+ import threading
22
+ import warnings
23
+
24
+ from git.compat import defenc, force_bytes, safe_decode
25
+ from git.exc import (
26
+ CommandError,
27
+ GitCommandError,
28
+ GitCommandNotFound,
29
+ UnsafeOptionError,
30
+ UnsafeProtocolError,
31
+ )
32
+ from git.util import (
33
+ cygpath,
34
+ expand_path,
35
+ is_cygwin_git,
36
+ patch_env,
37
+ remove_password_if_present,
38
+ stream_copy,
39
+ )
40
+
41
+ # typing ---------------------------------------------------------------------------
42
+
43
+ from typing import (
44
+ Any,
45
+ AnyStr,
46
+ BinaryIO,
47
+ Callable,
48
+ Dict,
49
+ IO,
50
+ Iterator,
51
+ List,
52
+ Mapping,
53
+ Optional,
54
+ Sequence,
55
+ TYPE_CHECKING,
56
+ TextIO,
57
+ Tuple,
58
+ Union,
59
+ cast,
60
+ overload,
61
+ )
62
+
63
+ if sys.version_info >= (3, 10):
64
+ from typing import TypeAlias
65
+ else:
66
+ from typing_extensions import TypeAlias
67
+
68
+ from git.types import Literal, PathLike, TBD
69
+
70
+ if TYPE_CHECKING:
71
+ from git.diff import DiffIndex
72
+ from git.repo.base import Repo
73
+
74
+ # ---------------------------------------------------------------------------------
75
+
76
+ execute_kwargs = {
77
+ "istream",
78
+ "with_extended_output",
79
+ "with_exceptions",
80
+ "as_process",
81
+ "output_stream",
82
+ "stdout_as_string",
83
+ "kill_after_timeout",
84
+ "with_stdout",
85
+ "universal_newlines",
86
+ "shell",
87
+ "env",
88
+ "max_chunk_size",
89
+ "strip_newline_in_stdout",
90
+ }
91
+
92
+ _logger = logging.getLogger(__name__)
93
+
94
+
95
+ # ==============================================================================
96
+ ## @name Utilities
97
+ # ------------------------------------------------------------------------------
98
+ # Documentation
99
+ ## @{
100
+
101
+
102
+ def handle_process_output(
103
+ process: "Git.AutoInterrupt" | Popen,
104
+ stdout_handler: Union[
105
+ None,
106
+ Callable[[AnyStr], None],
107
+ Callable[[List[AnyStr]], None],
108
+ Callable[[bytes, "Repo", "DiffIndex"], None],
109
+ ],
110
+ stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
111
+ finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
112
+ decode_streams: bool = True,
113
+ kill_after_timeout: Union[None, float] = None,
114
+ ) -> None:
115
+ R"""Register for notifications to learn that process output is ready to read, and
116
+ dispatch lines to the respective line handlers.
117
+
118
+ This function returns once the finalizer returns.
119
+
120
+ :param process:
121
+ :class:`subprocess.Popen` instance.
122
+
123
+ :param stdout_handler:
124
+ f(stdout_line_string), or ``None``.
125
+
126
+ :param stderr_handler:
127
+ f(stderr_line_string), or ``None``.
128
+
129
+ :param finalizer:
130
+ f(proc) - wait for proc to finish.
131
+
132
+ :param decode_streams:
133
+ Assume stdout/stderr streams are binary and decode them before pushing their
134
+ contents to handlers.
135
+
136
+ This defaults to ``True``. Set it to ``False`` if:
137
+
138
+ - ``universal_newlines == True``, as then streams are in text mode, or
139
+ - decoding must happen later, such as for :class:`~git.diff.Diff`\s.
140
+
141
+ :param kill_after_timeout:
142
+ :class:`float` or ``None``, Default = ``None``
143
+
144
+ To specify a timeout in seconds for the git command, after which the process
145
+ should be killed.
146
+ """
147
+
148
+ # Use 2 "pump" threads and wait for both to finish.
149
+ def pump_stream(
150
+ cmdline: List[str],
151
+ name: str,
152
+ stream: Union[BinaryIO, TextIO],
153
+ is_decode: bool,
154
+ handler: Union[None, Callable[[Union[bytes, str]], None]],
155
+ ) -> None:
156
+ try:
157
+ for line in stream:
158
+ if handler:
159
+ if is_decode:
160
+ assert isinstance(line, bytes)
161
+ line_str = line.decode(defenc)
162
+ handler(line_str)
163
+ else:
164
+ handler(line)
165
+
166
+ except Exception as ex:
167
+ _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
168
+ if "I/O operation on closed file" not in str(ex):
169
+ # Only reraise if the error was not due to the stream closing.
170
+ raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
171
+ finally:
172
+ stream.close()
173
+
174
+ if hasattr(process, "proc"):
175
+ process = cast("Git.AutoInterrupt", process)
176
+ cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "")
177
+ p_stdout = process.proc.stdout if process.proc else None
178
+ p_stderr = process.proc.stderr if process.proc else None
179
+ else:
180
+ process = cast(Popen, process) # type: ignore[redundant-cast]
181
+ cmdline = getattr(process, "args", "")
182
+ p_stdout = process.stdout
183
+ p_stderr = process.stderr
184
+
185
+ if not isinstance(cmdline, (tuple, list)):
186
+ cmdline = cmdline.split()
187
+
188
+ pumps: List[Tuple[str, IO, Callable[..., None] | None]] = []
189
+ if p_stdout:
190
+ pumps.append(("stdout", p_stdout, stdout_handler))
191
+ if p_stderr:
192
+ pumps.append(("stderr", p_stderr, stderr_handler))
193
+
194
+ threads: List[threading.Thread] = []
195
+
196
+ for name, stream, handler in pumps:
197
+ t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler))
198
+ t.daemon = True
199
+ t.start()
200
+ threads.append(t)
201
+
202
+ # FIXME: Why join? Will block if stdin needs feeding...
203
+ for t in threads:
204
+ t.join(timeout=kill_after_timeout)
205
+ if t.is_alive():
206
+ if isinstance(process, Git.AutoInterrupt):
207
+ process._terminate()
208
+ else: # Don't want to deal with the other case.
209
+ raise RuntimeError(
210
+ "Thread join() timed out in cmd.handle_process_output()."
211
+ f" kill_after_timeout={kill_after_timeout} seconds"
212
+ )
213
+ if stderr_handler:
214
+ error_str: Union[str, bytes] = (
215
+ f"error: process killed because it timed out. kill_after_timeout={kill_after_timeout} seconds"
216
+ )
217
+ if not decode_streams and isinstance(p_stderr, BinaryIO):
218
+ # Assume stderr_handler needs binary input.
219
+ error_str = cast(str, error_str)
220
+ error_str = error_str.encode()
221
+ # We ignore typing on the next line because mypy does not like the way
222
+ # we inferred that stderr takes str or bytes.
223
+ stderr_handler(error_str) # type: ignore[arg-type]
224
+
225
+ if finalizer:
226
+ finalizer(process)
227
+
228
+
229
+ safer_popen: Callable[..., Popen]
230
+
231
+ if sys.platform == "win32":
232
+
233
+ def _safer_popen_windows(
234
+ command: Union[str, Sequence[Any]],
235
+ *,
236
+ shell: bool = False,
237
+ env: Optional[Mapping[str, str]] = None,
238
+ **kwargs: Any,
239
+ ) -> Popen:
240
+ """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the
241
+ search.
242
+
243
+ This avoids an untrusted search path condition where a file like ``git.exe`` in
244
+ a malicious repository would be run when GitPython operates on the repository.
245
+ The process using GitPython may have an untrusted repository's working tree as
246
+ its current working directory. Some operations may temporarily change to that
247
+ directory before running a subprocess. In addition, while by default GitPython
248
+ does not run external commands with a shell, it can be made to do so, in which
249
+ case the CWD of the subprocess, which GitPython usually sets to a repository
250
+ working tree, can itself be searched automatically by the shell. This wrapper
251
+ covers all those cases.
252
+
253
+ :note:
254
+ This currently works by setting the
255
+ :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during
256
+ subprocess creation. It also takes care of passing Windows-specific process
257
+ creation flags, but that is unrelated to path search.
258
+
259
+ :note:
260
+ The current implementation contains a race condition on :attr:`os.environ`.
261
+ GitPython isn't thread-safe, but a program using it on one thread should
262
+ ideally be able to mutate :attr:`os.environ` on another, without
263
+ unpredictable results. See comments in:
264
+ https://github.com/gitpython-developers/GitPython/pull/1650
265
+ """
266
+ # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards.
267
+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
268
+ # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
269
+ creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
270
+
271
+ # When using a shell, the shell is the direct subprocess, so the variable must
272
+ # be set in its environment, to affect its search behavior.
273
+ if shell:
274
+ # The original may be immutable, or the caller may reuse it. Mutate a copy.
275
+ env = {} if env is None else dict(env)
276
+ env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be any value.
277
+
278
+ # When not using a shell, the current process does the search in a
279
+ # CreateProcessW API call, so the variable must be set in our environment. With
280
+ # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
281
+ # is patched. In Python versions where it is unpatched, in the rare case the
282
+ # ComSpec environment variable is unset, the search for the shell itself is
283
+ # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
284
+ # is simpler and protects against that. (As above, the "1" can be any value.)
285
+ with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
286
+ return Popen(
287
+ command,
288
+ shell=shell,
289
+ env=env,
290
+ creationflags=creationflags,
291
+ **kwargs,
292
+ )
293
+
294
+ safer_popen = _safer_popen_windows
295
+ else:
296
+ safer_popen = Popen
297
+
298
+
299
+ def dashify(string: str) -> str:
300
+ return string.replace("_", "-")
301
+
302
+
303
+ def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]:
304
+ return {s: getattr(self, s) for s in self.__slots__ if s not in exclude}
305
+
306
+
307
+ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None:
308
+ for k, v in d.items():
309
+ setattr(self, k, v)
310
+ for k in excluded:
311
+ setattr(self, k, None)
312
+
313
+
314
+ ## -- End Utilities -- @}
315
+
316
+
317
+ class _AutoInterrupt:
318
+ """Process wrapper that terminates the wrapped process on finalization.
319
+
320
+ This kills/interrupts the stored process instance once this instance goes out of
321
+ scope. It is used to prevent processes piling up in case iterators stop reading.
322
+
323
+ All attributes are wired through to the contained process object.
324
+
325
+ The wait method is overridden to perform automatic status code checking and possibly
326
+ raise.
327
+ """
328
+
329
+ __slots__ = ("proc", "args", "status")
330
+
331
+ # If this is non-zero it will override any status code during _terminate, used
332
+ # to prevent race conditions in testing.
333
+ _status_code_if_terminate: int = 0
334
+
335
+ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
336
+ self.proc = proc
337
+ self.args = args
338
+ self.status: Union[int, None] = None
339
+
340
+ def _terminate(self) -> None:
341
+ """Terminate the underlying process."""
342
+ if self.proc is None:
343
+ return
344
+
345
+ proc = self.proc
346
+ self.proc = None
347
+ if proc.stdin:
348
+ proc.stdin.close()
349
+ if proc.stdout:
350
+ proc.stdout.close()
351
+ if proc.stderr:
352
+ proc.stderr.close()
353
+ # Did the process finish already so we have a return code?
354
+ try:
355
+ if proc.poll() is not None:
356
+ self.status = self._status_code_if_terminate or proc.poll()
357
+ return
358
+ except OSError as ex:
359
+ _logger.info("Ignored error after process had died: %r", ex)
360
+
361
+ # It can be that nothing really exists anymore...
362
+ if os is None or getattr(os, "kill", None) is None:
363
+ return
364
+
365
+ # Try to kill it.
366
+ try:
367
+ proc.terminate()
368
+ status = proc.wait() # Ensure the process goes away.
369
+
370
+ self.status = self._status_code_if_terminate or status
371
+ except OSError as ex:
372
+ _logger.info("Ignored error after process had died: %r", ex)
373
+ # END exception handling
374
+
375
+ def __del__(self) -> None:
376
+ self._terminate()
377
+
378
+ def __getattr__(self, attr: str) -> Any:
379
+ return getattr(self.proc, attr)
380
+
381
+ # TODO: Bad choice to mimic `proc.wait()` but with different args.
382
+ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
383
+ """Wait for the process and return its status code.
384
+
385
+ :param stderr:
386
+ Previously read value of stderr, in case stderr is already closed.
387
+
388
+ :warn:
389
+ May deadlock if output or error pipes are used and not handled separately.
390
+
391
+ :raise git.exc.GitCommandError:
392
+ If the return status is not 0.
393
+ """
394
+ if stderr is None:
395
+ stderr_b = b""
396
+ stderr_b = force_bytes(data=stderr, encoding="utf-8")
397
+ status: Union[int, None]
398
+ if self.proc is not None:
399
+ status = self.proc.wait()
400
+ p_stderr = self.proc.stderr
401
+ else: # Assume the underlying proc was killed earlier or never existed.
402
+ status = self.status
403
+ p_stderr = None
404
+
405
+ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
406
+ if stream:
407
+ try:
408
+ return stderr_b + force_bytes(stream.read())
409
+ except (OSError, ValueError):
410
+ return stderr_b or b""
411
+ else:
412
+ return stderr_b or b""
413
+
414
+ # END status handling
415
+
416
+ if status != 0:
417
+ errstr = read_all_from_possibly_closed_stream(p_stderr)
418
+ _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
419
+ raise GitCommandError(remove_password_if_present(self.args), status, errstr)
420
+ return status
421
+
422
+
423
+ _AutoInterrupt.__name__ = "AutoInterrupt"
424
+ _AutoInterrupt.__qualname__ = "Git.AutoInterrupt"
425
+
426
+
427
+ class _CatFileContentStream:
428
+ """Object representing a sized read-only stream returning the contents of
429
+ an object.
430
+
431
+ This behaves like a stream, but counts the data read and simulates an empty stream
432
+ once our sized content region is empty.
433
+
434
+ If not all data are read to the end of the object's lifetime, we read the rest to
435
+ ensure the underlying stream continues to work.
436
+ """
437
+
438
+ __slots__ = ("_stream", "_nbr", "_size")
439
+
440
+ def __init__(self, size: int, stream: IO[bytes]) -> None:
441
+ self._stream = stream
442
+ self._size = size
443
+ self._nbr = 0 # Number of bytes read.
444
+
445
+ # Special case: If the object is empty, has null bytes, get the final
446
+ # newline right away.
447
+ if size == 0:
448
+ stream.read(1)
449
+ # END handle empty streams
450
+
451
+ def read(self, size: int = -1) -> bytes:
452
+ bytes_left = self._size - self._nbr
453
+ if bytes_left == 0:
454
+ return b""
455
+ if size > -1:
456
+ # Ensure we don't try to read past our limit.
457
+ size = min(bytes_left, size)
458
+ else:
459
+ # They try to read all, make sure it's not more than what remains.
460
+ size = bytes_left
461
+ # END check early depletion
462
+ data = self._stream.read(size)
463
+ self._nbr += len(data)
464
+
465
+ # Check for depletion, read our final byte to make the stream usable by
466
+ # others.
467
+ if self._size - self._nbr == 0:
468
+ self._stream.read(1) # final newline
469
+ # END finish reading
470
+ return data
471
+
472
+ def readline(self, size: int = -1) -> bytes:
473
+ if self._nbr == self._size:
474
+ return b""
475
+
476
+ # Clamp size to lowest allowed value.
477
+ bytes_left = self._size - self._nbr
478
+ if size > -1:
479
+ size = min(bytes_left, size)
480
+ else:
481
+ size = bytes_left
482
+ # END handle size
483
+
484
+ data = self._stream.readline(size)
485
+ self._nbr += len(data)
486
+
487
+ # Handle final byte.
488
+ if self._size - self._nbr == 0:
489
+ self._stream.read(1)
490
+ # END finish reading
491
+
492
+ return data
493
+
494
+ def readlines(self, size: int = -1) -> List[bytes]:
495
+ if self._nbr == self._size:
496
+ return []
497
+
498
+ # Leave all additional logic to our readline method, we just check the size.
499
+ out = []
500
+ nbr = 0
501
+ while True:
502
+ line = self.readline()
503
+ if not line:
504
+ break
505
+ out.append(line)
506
+ if size > -1:
507
+ nbr += len(line)
508
+ if nbr > size:
509
+ break
510
+ # END handle size constraint
511
+ # END readline loop
512
+ return out
513
+
514
+ # skipcq: PYL-E0301
515
+ def __iter__(self) -> "Git.CatFileContentStream":
516
+ return self
517
+
518
+ def __next__(self) -> bytes:
519
+ line = self.readline()
520
+ if not line:
521
+ raise StopIteration
522
+
523
+ return line
524
+
525
+ next = __next__
526
+
527
+ def __del__(self) -> None:
528
+ bytes_left = self._size - self._nbr
529
+ if bytes_left:
530
+ # Read and discard - seeking is impossible within a stream.
531
+ # This includes any terminating newline.
532
+ self._stream.read(bytes_left + 1)
533
+ # END handle incomplete read
534
+
535
+
536
+ _CatFileContentStream.__name__ = "CatFileContentStream"
537
+ _CatFileContentStream.__qualname__ = "Git.CatFileContentStream"
538
+
539
+
540
+ _USE_SHELL_DEFAULT_MESSAGE = (
541
+ "Git.USE_SHELL is deprecated, because only its default value of False is safe. "
542
+ "It will be removed in a future release."
543
+ )
544
+
545
+ _USE_SHELL_DANGER_MESSAGE = (
546
+ "Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
547
+ "shell syntax cannot usually be accounted for. This can result in a command "
548
+ "injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
549
+ "and will be removed in a future release."
550
+ )
551
+
552
+
553
+ def _warn_use_shell(*, extra_danger: bool) -> None:
554
+ warnings.warn(
555
+ _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
556
+ DeprecationWarning,
557
+ stacklevel=3,
558
+ )
559
+
560
+
561
+ class _GitMeta(type):
562
+ """Metaclass for :class:`Git`.
563
+
564
+ This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
565
+ """
566
+
567
+ def __getattribute(cls, name: str) -> Any:
568
+ if name == "USE_SHELL":
569
+ _warn_use_shell(extra_danger=False)
570
+ return super().__getattribute__(name)
571
+
572
+ def __setattr(cls, name: str, value: Any) -> Any:
573
+ if name == "USE_SHELL":
574
+ _warn_use_shell(extra_danger=value)
575
+ super().__setattr__(name, value)
576
+
577
+ if not TYPE_CHECKING:
578
+ # To preserve static checking for undefined/misspelled attributes while letting
579
+ # the methods' bodies be type-checked, these are defined as non-special methods,
580
+ # then bound to special names out of view of static type checkers. (The original
581
+ # names invoke name mangling (leading "__") to avoid confusion in other scopes.)
582
+ __getattribute__ = __getattribute
583
+ __setattr__ = __setattr
584
+
585
+
586
+ GitMeta = _GitMeta
587
+ """Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
588
+
589
+ Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
590
+ custom metaclass is not documented and may change at any time. This statically checkable
591
+ metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
592
+ used. Code that benefits from it is likely to be remain brittle even if it is used.
593
+
594
+ In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
595
+ callable attributes representing git subcommands, it rarely makes sense to inherit from
596
+ :class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
597
+ to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
598
+ as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
599
+ might have to be introduced to solve a metaclass conflict, is not recommended.
600
+
601
+ :note:
602
+ The correct static type of the :class:`Git` class itself, and any subclasses, is
603
+ ``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
604
+
605
+ :class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
606
+ intended or otherwise possible to use. This alias is truly only for very rare and
607
+ inherently precarious situations where it is necessary to deal with the metaclass
608
+ explicitly.
609
+ """
610
+
611
+
612
+ class Git(metaclass=_GitMeta):
613
+ """The Git class manages communication with the Git binary.
614
+
615
+ It provides a convenient interface to calling the Git binary, such as in::
616
+
617
+ g = Git( git_dir )
618
+ g.init() # calls 'git init' program
619
+ rval = g.ls_files() # calls 'git ls-files' program
620
+
621
+ Debugging:
622
+
623
+ * Set the :envvar:`GIT_PYTHON_TRACE` environment variable to print each invocation
624
+ of the command to stdout.
625
+ * Set its value to ``full`` to see details about the returned values.
626
+ """
627
+
628
+ __slots__ = (
629
+ "_working_dir",
630
+ "cat_file_all",
631
+ "cat_file_header",
632
+ "_version_info",
633
+ "_version_info_token",
634
+ "_git_options",
635
+ "_persistent_git_options",
636
+ "_environment",
637
+ )
638
+
639
+ _excluded_ = (
640
+ "cat_file_all",
641
+ "cat_file_header",
642
+ "_version_info",
643
+ "_version_info_token",
644
+ )
645
+
646
+ re_unsafe_protocol = re.compile(r"(.+)::.+")
647
+
648
+ def __getstate__(self) -> Dict[str, Any]:
649
+ return slots_to_dict(self, exclude=self._excluded_)
650
+
651
+ def __setstate__(self, d: Dict[str, Any]) -> None:
652
+ dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_)
653
+
654
+ # CONFIGURATION
655
+
656
+ git_exec_name = "git"
657
+ """Default git command that should work on Linux, Windows, and other systems."""
658
+
659
+ GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
660
+ """Enables debugging of GitPython's git commands."""
661
+
662
+ USE_SHELL: bool = False
663
+ """Deprecated. If set to ``True``, a shell will be used when executing git commands.
664
+
665
+ Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
666
+ functions should be updated to use the default value of ``False`` instead. ``True``
667
+ is unsafe unless the effect of syntax treated specially by the shell is fully
668
+ considered and accounted for, which is not possible under most circumstances. As
669
+ detailed below, it is also no longer needed, even where it had been in the past.
670
+
671
+ It is in many if not most cases a command injection vulnerability for an application
672
+ to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
673
+ fragment of text to make its way into any part of any argument to any git command
674
+ (including paths, branch names, etc.) can cause the shell to read and write
675
+ arbitrary files and execute arbitrary commands. Innocent input may also accidentally
676
+ contain special shell syntax, leading to inadvertent malfunctions.
677
+
678
+ In addition, how a value of ``True`` interacts with some aspects of GitPython's
679
+ operation is not precisely specified and may change without warning, even before
680
+ GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
681
+
682
+ * Whether or how GitPython automatically customizes the shell environment.
683
+
684
+ * Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
685
+ separate arguments even when ``shell=True``), this can be used with any GitPython
686
+ functionality other than direct calls to the :meth:`execute` method.
687
+
688
+ * Whether any GitPython feature that runs git commands ever attempts to partially
689
+ sanitize data a shell may treat specially. Currently this is not done.
690
+
691
+ Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
692
+ in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
693
+ GitPython solves that problem more robustly and safely by using the
694
+ ``CREATE_NO_WINDOW`` process creation flag on Windows.
695
+
696
+ Because Windows path search differs subtly based on whether a shell is used, in rare
697
+ cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
698
+ such as a batch file, from being found. To fix this, set the command name or full
699
+ path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
700
+ full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
701
+
702
+ Further reading:
703
+
704
+ * :meth:`Git.execute` (on the ``shell`` parameter).
705
+ * https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
706
+ * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
707
+ * https://github.com/python/cpython/issues/91558#issuecomment-1100942950
708
+ * https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
709
+ """
710
+
711
+ _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
712
+ _refresh_env_var = "GIT_PYTHON_REFRESH"
713
+
714
+ GIT_PYTHON_GIT_EXECUTABLE = None
715
+ """Provide the full path to the git executable. Otherwise it assumes git is in the
716
+ executable search path.
717
+
718
+ :note:
719
+ The git executable is actually found during the refresh step in the top level
720
+ ``__init__``. It can also be changed by explicitly calling :func:`git.refresh`.
721
+ """
722
+
723
+ _refresh_token = object() # Since None would match an initial _version_info_token.
724
+
725
+ @classmethod
726
+ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
727
+ """Update information about the git executable :class:`Git` objects will use.
728
+
729
+ Called by the :func:`git.refresh` function in the top level ``__init__``.
730
+
731
+ :param path:
732
+ Optional path to the git executable. If not absolute, it is resolved
733
+ immediately, relative to the current directory. (See note below.)
734
+
735
+ :note:
736
+ The top-level :func:`git.refresh` should be preferred because it calls this
737
+ method and may also update other state accordingly.
738
+
739
+ :note:
740
+ There are three different ways to specify the command that refreshing causes
741
+ to be used for git:
742
+
743
+ 1. Pass no `path` argument and do not set the
744
+ :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable. The command
745
+ name ``git`` is used. It is looked up in a path search by the system, in
746
+ each command run (roughly similar to how git is found when running
747
+ ``git`` commands manually). This is usually the desired behavior.
748
+
749
+ 2. Pass no `path` argument but set the :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
750
+ environment variable. The command given as the value of that variable is
751
+ used. This may be a simple command or an arbitrary path. It is looked up
752
+ in each command run. Setting :envvar:`GIT_PYTHON_GIT_EXECUTABLE` to
753
+ ``git`` has the same effect as not setting it.
754
+
755
+ 3. Pass a `path` argument. This path, if not absolute, is immediately
756
+ resolved, relative to the current directory. This resolution occurs at
757
+ the time of the refresh. When git commands are run, they are run using
758
+ that previously resolved path. If a `path` argument is passed, the
759
+ :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable is not
760
+ consulted.
761
+
762
+ :note:
763
+ Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
764
+ attribute, which can be read on the :class:`Git` class or any of its
765
+ instances to check what command is used to run git. This attribute should
766
+ not be confused with the related :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
767
+ environment variable. The class attribute is set no matter how refreshing is
768
+ performed.
769
+ """
770
+ # Discern which path to refresh with.
771
+ if path is not None:
772
+ new_git = os.path.expanduser(path)
773
+ new_git = os.path.abspath(new_git)
774
+ else:
775
+ new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name)
776
+
777
+ # Keep track of the old and new git executable path.
778
+ old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
779
+ old_refresh_token = cls._refresh_token
780
+ cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
781
+ cls._refresh_token = object()
782
+
783
+ # Test if the new git executable path is valid. A GitCommandNotFound error is
784
+ # raised by us. A PermissionError is raised if the git executable cannot be
785
+ # executed for whatever reason.
786
+ has_git = False
787
+ try:
788
+ cls().version()
789
+ has_git = True
790
+ except (GitCommandNotFound, PermissionError):
791
+ pass
792
+
793
+ # Warn or raise exception if test failed.
794
+ if not has_git:
795
+ err = (
796
+ dedent(
797
+ """\
798
+ Bad git executable.
799
+ The git executable must be specified in one of the following ways:
800
+ - be included in your $PATH
801
+ - be set via $%s
802
+ - explicitly set via git.refresh(<full-path-to-git-executable>)
803
+ """
804
+ )
805
+ % cls._git_exec_env_var
806
+ )
807
+
808
+ # Revert to whatever the old_git was.
809
+ cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
810
+ cls._refresh_token = old_refresh_token
811
+
812
+ if old_git is None:
813
+ # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
814
+ # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
815
+
816
+ # Determine what the user wants to happen during the initial refresh. We
817
+ # expect GIT_PYTHON_REFRESH to either be unset or be one of the
818
+ # following values:
819
+ #
820
+ # 0|q|quiet|s|silence|silent|n|none
821
+ # 1|w|warn|warning|l|log
822
+ # 2|r|raise|e|error|exception
823
+
824
+ mode = os.environ.get(cls._refresh_env_var, "raise").lower()
825
+
826
+ quiet = ["quiet", "q", "silence", "s", "silent", "none", "n", "0"]
827
+ warn = ["warn", "w", "warning", "log", "l", "1"]
828
+ error = ["error", "e", "exception", "raise", "r", "2"]
829
+
830
+ if mode in quiet:
831
+ pass
832
+ elif mode in warn or mode in error:
833
+ err = dedent(
834
+ """\
835
+ %s
836
+ All git commands will error until this is rectified.
837
+
838
+ This initial message can be silenced or aggravated in the future by setting the
839
+ $%s environment variable. Use one of the following values:
840
+ - %s: for no message or exception
841
+ - %s: for a warning message (logging level CRITICAL, displayed by default)
842
+ - %s: for a raised exception
843
+
844
+ Example:
845
+ export %s=%s
846
+ """
847
+ ) % (
848
+ err,
849
+ cls._refresh_env_var,
850
+ "|".join(quiet),
851
+ "|".join(warn),
852
+ "|".join(error),
853
+ cls._refresh_env_var,
854
+ quiet[0],
855
+ )
856
+
857
+ if mode in warn:
858
+ _logger.critical(err)
859
+ else:
860
+ raise ImportError(err)
861
+ else:
862
+ err = dedent(
863
+ """\
864
+ %s environment variable has been set but it has been set with an invalid value.
865
+
866
+ Use only the following values:
867
+ - %s: for no message or exception
868
+ - %s: for a warning message (logging level CRITICAL, displayed by default)
869
+ - %s: for a raised exception
870
+ """
871
+ ) % (
872
+ cls._refresh_env_var,
873
+ "|".join(quiet),
874
+ "|".join(warn),
875
+ "|".join(error),
876
+ )
877
+ raise ImportError(err)
878
+
879
+ # We get here if this was the initial refresh and the refresh mode was
880
+ # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
881
+ # discern the difference between the first refresh at import time
882
+ # and subsequent calls to git.refresh or this refresh method.
883
+ cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
884
+ else:
885
+ # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
886
+ # None) we raise an exception.
887
+ raise GitCommandNotFound(new_git, err)
888
+
889
+ return has_git
890
+
891
+ @classmethod
892
+ def is_cygwin(cls) -> bool:
893
+ return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE)
894
+
895
+ @overload
896
+ @classmethod
897
+ def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ...
898
+
899
+ @overload
900
+ @classmethod
901
+ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ...
902
+
903
+ @classmethod
904
+ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
905
+ """Remove any backslashes from URLs to be written in config files.
906
+
907
+ Windows might create config files containing paths with backslashes, but git
908
+ stops liking them as it will escape the backslashes. Hence we undo the escaping
909
+ just to be sure.
910
+ """
911
+ if is_cygwin is None:
912
+ is_cygwin = cls.is_cygwin()
913
+
914
+ if is_cygwin:
915
+ url = cygpath(url)
916
+ else:
917
+ url = os.path.expandvars(url)
918
+ if url.startswith("~"):
919
+ url = os.path.expanduser(url)
920
+ url = url.replace("\\\\", "\\").replace("\\", "/")
921
+ return url
922
+
923
+ @classmethod
924
+ def check_unsafe_protocols(cls, url: str) -> None:
925
+ """Check for unsafe protocols.
926
+
927
+ Apart from the usual protocols (http, git, ssh), Git allows "remote helpers"
928
+ that have the form ``<transport>::<address>``. One of these helpers (``ext::``)
929
+ can be used to invoke any arbitrary command.
930
+
931
+ See:
932
+
933
+ - https://git-scm.com/docs/gitremote-helpers
934
+ - https://git-scm.com/docs/git-remote-ext
935
+ """
936
+ match = cls.re_unsafe_protocol.match(url)
937
+ if match:
938
+ protocol = match.group(1)
939
+ raise UnsafeProtocolError(
940
+ f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
941
+ )
942
+
943
+ @classmethod
944
+ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
945
+ """Check for unsafe options.
946
+
947
+ Some options that are passed to ``git <command>`` can be used to execute
948
+ arbitrary commands. These are blocked by default.
949
+ """
950
+ # Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
951
+ # check if they start with "--foo" or if they are equal to "foo".
952
+ bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
953
+ for option in options:
954
+ for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
955
+ if option.startswith(unsafe_option) or option == bare_option:
956
+ raise UnsafeOptionError(
957
+ f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
958
+ )
959
+
960
+ AutoInterrupt: TypeAlias = _AutoInterrupt
961
+
962
+ CatFileContentStream: TypeAlias = _CatFileContentStream
963
+
964
+ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
965
+ """Initialize this instance with:
966
+
967
+ :param working_dir:
968
+ Git directory we should work in. If ``None``, we always work in the current
969
+ directory as returned by :func:`os.getcwd`.
970
+ This is meant to be the working tree directory if available, or the
971
+ ``.git`` directory in case of bare repositories.
972
+ """
973
+ super().__init__()
974
+ self._working_dir = expand_path(working_dir)
975
+ self._git_options: Union[List[str], Tuple[str, ...]] = ()
976
+ self._persistent_git_options: List[str] = []
977
+
978
+ # Extra environment variables to pass to git commands
979
+ self._environment: Dict[str, str] = {}
980
+
981
+ # Cached version slots
982
+ self._version_info: Union[Tuple[int, ...], None] = None
983
+ self._version_info_token: object = None
984
+
985
+ # Cached command slots
986
+ self.cat_file_header: Union[None, TBD] = None
987
+ self.cat_file_all: Union[None, TBD] = None
988
+
989
+ def __getattribute__(self, name: str) -> Any:
990
+ if name == "USE_SHELL":
991
+ _warn_use_shell(extra_danger=False)
992
+ return super().__getattribute__(name)
993
+
994
+ def __getattr__(self, name: str) -> Any:
995
+ """A convenience method as it allows to call the command as if it was an object.
996
+
997
+ :return:
998
+ Callable object that will execute call :meth:`_call_process` with your
999
+ arguments.
1000
+ """
1001
+ if name.startswith("_"):
1002
+ return super().__getattribute__(name)
1003
+ return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
1004
+
1005
+ def set_persistent_git_options(self, **kwargs: Any) -> None:
1006
+ """Specify command line options to the git executable for subsequent
1007
+ subcommand calls.
1008
+
1009
+ :param kwargs:
1010
+ A dict of keyword arguments.
1011
+ These arguments are passed as in :meth:`_call_process`, but will be passed
1012
+ to the git command rather than the subcommand.
1013
+ """
1014
+
1015
+ self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
1016
+
1017
+ @property
1018
+ def working_dir(self) -> Union[None, PathLike]:
1019
+ """:return: Git directory we are working on"""
1020
+ return self._working_dir
1021
+
1022
+ @property
1023
+ def version_info(self) -> Tuple[int, ...]:
1024
+ """
1025
+ :return: Tuple with integers representing the major, minor and additional
1026
+ version numbers as parsed from :manpage:`git-version(1)`. Up to four fields
1027
+ are used.
1028
+
1029
+ This value is generated on demand and is cached.
1030
+ """
1031
+ # Refreshing is global, but version_info caching is per-instance.
1032
+ refresh_token = self._refresh_token # Copy token in case of concurrent refresh.
1033
+
1034
+ # Use the cached version if obtained after the most recent refresh.
1035
+ if self._version_info_token is refresh_token:
1036
+ assert self._version_info is not None, "Bug: corrupted token-check state"
1037
+ return self._version_info
1038
+
1039
+ # Run "git version" and parse it.
1040
+ process_version = self._call_process("version")
1041
+ version_string = process_version.split(" ")[2]
1042
+ version_fields = version_string.split(".")[:4]
1043
+ leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields)
1044
+ self._version_info = tuple(map(int, leading_numeric_fields))
1045
+
1046
+ # This value will be considered valid until the next refresh.
1047
+ self._version_info_token = refresh_token
1048
+ return self._version_info
1049
+
1050
+ @overload
1051
+ def execute(
1052
+ self,
1053
+ command: Union[str, Sequence[Any]],
1054
+ *,
1055
+ as_process: Literal[True],
1056
+ ) -> "AutoInterrupt": ...
1057
+
1058
+ @overload
1059
+ def execute(
1060
+ self,
1061
+ command: Union[str, Sequence[Any]],
1062
+ *,
1063
+ as_process: Literal[False] = False,
1064
+ stdout_as_string: Literal[True],
1065
+ ) -> Union[str, Tuple[int, str, str]]: ...
1066
+
1067
+ @overload
1068
+ def execute(
1069
+ self,
1070
+ command: Union[str, Sequence[Any]],
1071
+ *,
1072
+ as_process: Literal[False] = False,
1073
+ stdout_as_string: Literal[False] = False,
1074
+ ) -> Union[bytes, Tuple[int, bytes, str]]: ...
1075
+
1076
+ @overload
1077
+ def execute(
1078
+ self,
1079
+ command: Union[str, Sequence[Any]],
1080
+ *,
1081
+ with_extended_output: Literal[False],
1082
+ as_process: Literal[False],
1083
+ stdout_as_string: Literal[True],
1084
+ ) -> str: ...
1085
+
1086
+ @overload
1087
+ def execute(
1088
+ self,
1089
+ command: Union[str, Sequence[Any]],
1090
+ *,
1091
+ with_extended_output: Literal[False],
1092
+ as_process: Literal[False],
1093
+ stdout_as_string: Literal[False],
1094
+ ) -> bytes: ...
1095
+
1096
+ def execute(
1097
+ self,
1098
+ command: Union[str, Sequence[Any]],
1099
+ istream: Union[None, BinaryIO] = None,
1100
+ with_extended_output: bool = False,
1101
+ with_exceptions: bool = True,
1102
+ as_process: bool = False,
1103
+ output_stream: Union[None, BinaryIO] = None,
1104
+ stdout_as_string: bool = True,
1105
+ kill_after_timeout: Union[None, float] = None,
1106
+ with_stdout: bool = True,
1107
+ universal_newlines: bool = False,
1108
+ shell: Union[None, bool] = None,
1109
+ env: Union[None, Mapping[str, str]] = None,
1110
+ max_chunk_size: int = io.DEFAULT_BUFFER_SIZE,
1111
+ strip_newline_in_stdout: bool = True,
1112
+ **subprocess_kwargs: Any,
1113
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
1114
+ R"""Handle executing the command, and consume and return the returned
1115
+ information (stdout).
1116
+
1117
+ :param command:
1118
+ The command argument list to execute.
1119
+ It should be a sequence of program arguments, or a string. The
1120
+ program to execute is the first item in the args sequence or string.
1121
+
1122
+ :param istream:
1123
+ Standard input filehandle passed to :class:`subprocess.Popen`.
1124
+
1125
+ :param with_extended_output:
1126
+ Whether to return a (status, stdout, stderr) tuple.
1127
+
1128
+ :param with_exceptions:
1129
+ Whether to raise an exception when git returns a non-zero status.
1130
+
1131
+ :param as_process:
1132
+ Whether to return the created process instance directly from which
1133
+ streams can be read on demand. This will render `with_extended_output`
1134
+ and `with_exceptions` ineffective - the caller will have to deal with
1135
+ the details. It is important to note that the process will be placed
1136
+ into an :class:`AutoInterrupt` wrapper that will interrupt the process
1137
+ once it goes out of scope. If you use the command in iterators, you
1138
+ should pass the whole process instance instead of a single stream.
1139
+
1140
+ :param output_stream:
1141
+ If set to a file-like object, data produced by the git command will be
1142
+ copied to the given stream instead of being returned as a string.
1143
+ This feature only has any effect if `as_process` is ``False``.
1144
+
1145
+ :param stdout_as_string:
1146
+ If ``False``, the command's standard output will be bytes. Otherwise, it
1147
+ will be decoded into a string using the default encoding (usually UTF-8).
1148
+ The latter can fail, if the output contains binary data.
1149
+
1150
+ :param kill_after_timeout:
1151
+ Specifies a timeout in seconds for the git command, after which the process
1152
+ should be killed. This will have no effect if `as_process` is set to
1153
+ ``True``. It is set to ``None`` by default and will let the process run
1154
+ until the timeout is explicitly specified. Uses of this feature should be
1155
+ carefully considered, due to the following limitations:
1156
+
1157
+ 1. This feature is not supported at all on Windows.
1158
+ 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
1159
+ enumerate child processes, which is available on most GNU/Linux systems
1160
+ but not most others.
1161
+ 3. Deeper descendants do not receive signals, though they may sometimes
1162
+ terminate as a consequence of their parent processes being killed.
1163
+ 4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side
1164
+ effects on a repository. For example, stale locks in case of
1165
+ :manpage:`git-gc(1)` could render the repository incapable of accepting
1166
+ changes until the lock is manually removed.
1167
+
1168
+ :param with_stdout:
1169
+ If ``True``, default ``True``, we open stdout on the created process.
1170
+
1171
+ :param universal_newlines:
1172
+ If ``True``, pipes will be opened as text, and lines are split at all known
1173
+ line endings.
1174
+
1175
+ :param shell:
1176
+ Whether to invoke commands through a shell
1177
+ (see :class:`Popen(..., shell=True) <subprocess.Popen>`).
1178
+ If this is not ``None``, it overrides :attr:`USE_SHELL`.
1179
+
1180
+ Passing ``shell=True`` to this or any other GitPython function should be
1181
+ avoided, as it is unsafe under most circumstances. This is because it is
1182
+ typically not feasible to fully consider and account for the effect of shell
1183
+ expansions, especially when passing ``shell=True`` to other methods that
1184
+ forward it to :meth:`Git.execute`. Passing ``shell=True`` is also no longer
1185
+ needed (nor useful) to work around any known operating system specific
1186
+ issues.
1187
+
1188
+ :param env:
1189
+ A dictionary of environment variables to be passed to
1190
+ :class:`subprocess.Popen`.
1191
+
1192
+ :param max_chunk_size:
1193
+ Maximum number of bytes in one chunk of data passed to the `output_stream`
1194
+ in one invocation of its ``write()`` method. If the given number is not
1195
+ positive then the default value is used.
1196
+
1197
+ :param strip_newline_in_stdout:
1198
+ Whether to strip the trailing ``\n`` of the command stdout.
1199
+
1200
+ :param subprocess_kwargs:
1201
+ Keyword arguments to be passed to :class:`subprocess.Popen`. Please note
1202
+ that some of the valid kwargs are already set by this method; the ones you
1203
+ specify may not be the same ones.
1204
+
1205
+ :return:
1206
+ * str(output), if `extended_output` is ``False`` (Default)
1207
+ * tuple(int(status), str(stdout), str(stderr)),
1208
+ if `extended_output` is ``True``
1209
+
1210
+ If `output_stream` is ``True``, the stdout value will be your output stream:
1211
+
1212
+ * output_stream, if `extended_output` is ``False``
1213
+ * tuple(int(status), output_stream, str(stderr)),
1214
+ if `extended_output` is ``True``
1215
+
1216
+ Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
1217
+ output regardless of system language.
1218
+
1219
+ :raise git.exc.GitCommandError:
1220
+
1221
+ :note:
1222
+ If you add additional keyword arguments to the signature of this method, you
1223
+ must update the ``execute_kwargs`` variable housed in this module.
1224
+ """
1225
+ # Remove password for the command if present.
1226
+ redacted_command = remove_password_if_present(command)
1227
+ if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
1228
+ _logger.info(" ".join(redacted_command))
1229
+
1230
+ # Allow the user to have the command executed in their working dir.
1231
+ try:
1232
+ cwd = self._working_dir or os.getcwd() # type: Union[None, str]
1233
+ if not os.access(str(cwd), os.X_OK):
1234
+ cwd = None
1235
+ except FileNotFoundError:
1236
+ cwd = None
1237
+
1238
+ # Start the process.
1239
+ inline_env = env
1240
+ env = os.environ.copy()
1241
+ # Attempt to force all output to plain ASCII English, which is what some parsing
1242
+ # code may expect.
1243
+ # According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well
1244
+ # just to be sure.
1245
+ env["LANGUAGE"] = "C"
1246
+ env["LC_ALL"] = "C"
1247
+ env.update(self._environment)
1248
+ if inline_env is not None:
1249
+ env.update(inline_env)
1250
+
1251
+ if sys.platform == "win32":
1252
+ if kill_after_timeout is not None:
1253
+ raise GitCommandError(
1254
+ redacted_command,
1255
+ '"kill_after_timeout" feature is not supported on Windows.',
1256
+ )
1257
+ cmd_not_found_exception = OSError
1258
+ else:
1259
+ cmd_not_found_exception = FileNotFoundError
1260
+ # END handle
1261
+
1262
+ stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
1263
+ if shell is None:
1264
+ # Get the value of USE_SHELL with no deprecation warning. Do this without
1265
+ # warnings.catch_warnings, to avoid a race condition with application code
1266
+ # configuring warnings. The value could be looked up in type(self).__dict__
1267
+ # or Git.__dict__, but those can break under some circumstances. This works
1268
+ # the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1269
+ shell = super().__getattribute__("USE_SHELL")
1270
+ _logger.debug(
1271
+ "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
1272
+ redacted_command,
1273
+ cwd,
1274
+ "<valid stream>" if istream else "None",
1275
+ shell,
1276
+ universal_newlines,
1277
+ )
1278
+ try:
1279
+ proc = safer_popen(
1280
+ command,
1281
+ env=env,
1282
+ cwd=cwd,
1283
+ bufsize=-1,
1284
+ stdin=(istream or DEVNULL),
1285
+ stderr=PIPE,
1286
+ stdout=stdout_sink,
1287
+ shell=shell,
1288
+ universal_newlines=universal_newlines,
1289
+ encoding=defenc if universal_newlines else None,
1290
+ **subprocess_kwargs,
1291
+ )
1292
+ except cmd_not_found_exception as err:
1293
+ raise GitCommandNotFound(redacted_command, err) from err
1294
+ else:
1295
+ # Replace with a typeguard for Popen[bytes]?
1296
+ proc.stdout = cast(BinaryIO, proc.stdout)
1297
+ proc.stderr = cast(BinaryIO, proc.stderr)
1298
+
1299
+ if as_process:
1300
+ return self.AutoInterrupt(proc, command)
1301
+
1302
+ if sys.platform != "win32" and kill_after_timeout is not None:
1303
+ # Help mypy figure out this is not None even when used inside communicate().
1304
+ timeout = kill_after_timeout
1305
+
1306
+ def kill_process(pid: int) -> None:
1307
+ """Callback to kill a process.
1308
+
1309
+ This callback implementation would be ineffective and unsafe on Windows.
1310
+ """
1311
+ p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
1312
+ child_pids = []
1313
+ if p.stdout is not None:
1314
+ for line in p.stdout:
1315
+ if len(line.split()) > 0:
1316
+ local_pid = (line.split())[0]
1317
+ if local_pid.isdigit():
1318
+ child_pids.append(int(local_pid))
1319
+ try:
1320
+ os.kill(pid, signal.SIGKILL)
1321
+ for child_pid in child_pids:
1322
+ try:
1323
+ os.kill(child_pid, signal.SIGKILL)
1324
+ except OSError:
1325
+ pass
1326
+ # Tell the main routine that the process was killed.
1327
+ kill_check.set()
1328
+ except OSError:
1329
+ # It is possible that the process gets completed in the duration
1330
+ # after timeout happens and before we try to kill the process.
1331
+ pass
1332
+ return
1333
+
1334
+ def communicate() -> Tuple[AnyStr, AnyStr]:
1335
+ watchdog.start()
1336
+ out, err = proc.communicate()
1337
+ watchdog.cancel()
1338
+ if kill_check.is_set():
1339
+ err = 'Timeout: the command "%s" did not complete in %d secs.' % (
1340
+ " ".join(redacted_command),
1341
+ timeout,
1342
+ )
1343
+ if not universal_newlines:
1344
+ err = err.encode(defenc)
1345
+ return out, err
1346
+
1347
+ # END helpers
1348
+
1349
+ kill_check = threading.Event()
1350
+ watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
1351
+ else:
1352
+ communicate = proc.communicate
1353
+
1354
+ # Wait for the process to return.
1355
+ status = 0
1356
+ stdout_value: Union[str, bytes] = b""
1357
+ stderr_value: Union[str, bytes] = b""
1358
+ newline = "\n" if universal_newlines else b"\n"
1359
+ try:
1360
+ if output_stream is None:
1361
+ stdout_value, stderr_value = communicate()
1362
+ # Strip trailing "\n".
1363
+ if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
1364
+ stdout_value = stdout_value[:-1]
1365
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1366
+ stderr_value = stderr_value[:-1]
1367
+
1368
+ status = proc.returncode
1369
+ else:
1370
+ max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
1371
+ stream_copy(proc.stdout, output_stream, max_chunk_size)
1372
+ stdout_value = proc.stdout.read()
1373
+ stderr_value = proc.stderr.read()
1374
+ # Strip trailing "\n".
1375
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1376
+ stderr_value = stderr_value[:-1]
1377
+ status = proc.wait()
1378
+ # END stdout handling
1379
+ finally:
1380
+ proc.stdout.close()
1381
+ proc.stderr.close()
1382
+
1383
+ if self.GIT_PYTHON_TRACE == "full":
1384
+ cmdstr = " ".join(redacted_command)
1385
+
1386
+ def as_text(stdout_value: Union[bytes, str]) -> str:
1387
+ return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
1388
+
1389
+ # END as_text
1390
+
1391
+ if stderr_value:
1392
+ _logger.info(
1393
+ "%s -> %d; stdout: '%s'; stderr: '%s'",
1394
+ cmdstr,
1395
+ status,
1396
+ as_text(stdout_value),
1397
+ safe_decode(stderr_value),
1398
+ )
1399
+ elif stdout_value:
1400
+ _logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
1401
+ else:
1402
+ _logger.info("%s -> %d", cmdstr, status)
1403
+ # END handle debug printing
1404
+
1405
+ if with_exceptions and status != 0:
1406
+ raise GitCommandError(redacted_command, status, stderr_value, stdout_value)
1407
+
1408
+ if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream.
1409
+ stdout_value = safe_decode(stdout_value)
1410
+
1411
+ # Allow access to the command's status code.
1412
+ if with_extended_output:
1413
+ return (status, stdout_value, safe_decode(stderr_value))
1414
+ else:
1415
+ return stdout_value
1416
+
1417
+ def environment(self) -> Dict[str, str]:
1418
+ return self._environment
1419
+
1420
+ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
1421
+ """Set environment variables for future git invocations. Return all changed
1422
+ values in a format that can be passed back into this function to revert the
1423
+ changes.
1424
+
1425
+ Examples::
1426
+
1427
+ old_env = self.update_environment(PWD='/tmp')
1428
+ self.update_environment(**old_env)
1429
+
1430
+ :param kwargs:
1431
+ Environment variables to use for git processes.
1432
+
1433
+ :return:
1434
+ Dict that maps environment variables to their old values
1435
+ """
1436
+ old_env = {}
1437
+ for key, value in kwargs.items():
1438
+ # Set value if it is None.
1439
+ if value is not None:
1440
+ old_env[key] = self._environment.get(key)
1441
+ self._environment[key] = value
1442
+ # Remove key from environment if its value is None.
1443
+ elif key in self._environment:
1444
+ old_env[key] = self._environment[key]
1445
+ del self._environment[key]
1446
+ return old_env
1447
+
1448
+ @contextlib.contextmanager
1449
+ def custom_environment(self, **kwargs: Any) -> Iterator[None]:
1450
+ """A context manager around the above :meth:`update_environment` method to
1451
+ restore the environment back to its previous state after operation.
1452
+
1453
+ Examples::
1454
+
1455
+ with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
1456
+ repo.remotes.origin.fetch()
1457
+
1458
+ :param kwargs:
1459
+ See :meth:`update_environment`.
1460
+ """
1461
+ old_env = self.update_environment(**kwargs)
1462
+ try:
1463
+ yield
1464
+ finally:
1465
+ self.update_environment(**old_env)
1466
+
1467
+ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]:
1468
+ if len(name) == 1:
1469
+ if value is True:
1470
+ return ["-%s" % name]
1471
+ elif value not in (False, None):
1472
+ if split_single_char_options:
1473
+ return ["-%s" % name, "%s" % value]
1474
+ else:
1475
+ return ["-%s%s" % (name, value)]
1476
+ else:
1477
+ if value is True:
1478
+ return ["--%s" % dashify(name)]
1479
+ elif value is not False and value is not None:
1480
+ return ["--%s=%s" % (dashify(name), value)]
1481
+ return []
1482
+
1483
+ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
1484
+ """Transform Python-style kwargs into git command line options."""
1485
+ args = []
1486
+ for k, v in kwargs.items():
1487
+ if isinstance(v, (list, tuple)):
1488
+ for value in v:
1489
+ args += self.transform_kwarg(k, value, split_single_char_options)
1490
+ else:
1491
+ args += self.transform_kwarg(k, v, split_single_char_options)
1492
+ return args
1493
+
1494
+ @classmethod
1495
+ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
1496
+ outlist = []
1497
+ if isinstance(arg_list, (list, tuple)):
1498
+ for arg in arg_list:
1499
+ outlist.extend(cls._unpack_args(arg))
1500
+ else:
1501
+ outlist.append(str(arg_list))
1502
+
1503
+ return outlist
1504
+
1505
+ def __call__(self, **kwargs: Any) -> "Git":
1506
+ """Specify command line options to the git executable for a subcommand call.
1507
+
1508
+ :param kwargs:
1509
+ A dict of keyword arguments.
1510
+ These arguments are passed as in :meth:`_call_process`, but will be passed
1511
+ to the git command rather than the subcommand.
1512
+
1513
+ Examples::
1514
+
1515
+ git(work_tree='/tmp').difftool()
1516
+ """
1517
+ self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
1518
+ return self
1519
+
1520
+ @overload
1521
+ def _call_process(
1522
+ self, method: str, *args: None, **kwargs: None
1523
+ ) -> str: ... # If no args were given, execute the call with all defaults.
1524
+
1525
+ @overload
1526
+ def _call_process(
1527
+ self,
1528
+ method: str,
1529
+ istream: int,
1530
+ as_process: Literal[True],
1531
+ *args: Any,
1532
+ **kwargs: Any,
1533
+ ) -> "Git.AutoInterrupt": ...
1534
+
1535
+ @overload
1536
+ def _call_process(
1537
+ self, method: str, *args: Any, **kwargs: Any
1538
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ...
1539
+
1540
+ def _call_process(
1541
+ self, method: str, *args: Any, **kwargs: Any
1542
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
1543
+ """Run the given git command with the specified arguments and return the result
1544
+ as a string.
1545
+
1546
+ :param method:
1547
+ The command. Contained ``_`` characters will be converted to hyphens, such
1548
+ as in ``ls_files`` to call ``ls-files``.
1549
+
1550
+ :param args:
1551
+ The list of arguments. If ``None`` is included, it will be pruned.
1552
+ This allows your commands to call git more conveniently, as ``None`` is
1553
+ realized as non-existent.
1554
+
1555
+ :param kwargs:
1556
+ Contains key-values for the following:
1557
+
1558
+ - The :meth:`execute()` kwds, as listed in ``execute_kwargs``.
1559
+ - "Command options" to be converted by :meth:`transform_kwargs`.
1560
+ - The ``insert_kwargs_after`` key which its value must match one of
1561
+ ``*args``.
1562
+
1563
+ It also contains any command options, to be appended after the matched arg.
1564
+
1565
+ Examples::
1566
+
1567
+ git.rev_list('master', max_count=10, header=True)
1568
+
1569
+ turns into::
1570
+
1571
+ git rev-list max-count 10 --header master
1572
+
1573
+ :return:
1574
+ Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
1575
+ default (especially ``as_process = False``, ``stdout_as_string = True``) and
1576
+ return :class:`str`.
1577
+ """
1578
+ # Handle optional arguments prior to calling transform_kwargs.
1579
+ # Otherwise these'll end up in args, which is bad.
1580
+ exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
1581
+ opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
1582
+
1583
+ insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None)
1584
+
1585
+ # Prepare the argument list.
1586
+
1587
+ opt_args = self.transform_kwargs(**opts_kwargs)
1588
+ ext_args = self._unpack_args([a for a in args if a is not None])
1589
+
1590
+ if insert_after_this_arg is None:
1591
+ args_list = opt_args + ext_args
1592
+ else:
1593
+ try:
1594
+ index = ext_args.index(insert_after_this_arg)
1595
+ except ValueError as err:
1596
+ raise ValueError(
1597
+ "Couldn't find argument '%s' in args %s to insert cmd options after"
1598
+ % (insert_after_this_arg, str(ext_args))
1599
+ ) from err
1600
+ # END handle error
1601
+ args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :]
1602
+ # END handle opts_kwargs
1603
+
1604
+ call = [self.GIT_PYTHON_GIT_EXECUTABLE]
1605
+
1606
+ # Add persistent git options.
1607
+ call.extend(self._persistent_git_options)
1608
+
1609
+ # Add the git options, then reset to empty to avoid side effects.
1610
+ call.extend(self._git_options)
1611
+ self._git_options = ()
1612
+
1613
+ call.append(dashify(method))
1614
+ call.extend(args_list)
1615
+
1616
+ return self.execute(call, **exec_kwargs)
1617
+
1618
+ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
1619
+ """
1620
+ :param header_line:
1621
+ A line of the form::
1622
+
1623
+ <hex_sha> type_string size_as_int
1624
+
1625
+ :return:
1626
+ (hex_sha, type_string, size_as_int)
1627
+
1628
+ :raise ValueError:
1629
+ If the header contains indication for an error due to incorrect input sha.
1630
+ """
1631
+ tokens = header_line.split()
1632
+ if len(tokens) != 3:
1633
+ if not tokens:
1634
+ err_msg = (
1635
+ f"SHA is empty, possible dubious ownership in the repository "
1636
+ f"""at {self._working_dir}.\n If this is unintended run:\n\n """
1637
+ f""" "git config --global --add safe.directory {self._working_dir}" """
1638
+ )
1639
+ raise ValueError(err_msg)
1640
+ else:
1641
+ raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip()))
1642
+ # END handle actual return value
1643
+ # END error handling
1644
+
1645
+ if len(tokens[0]) != 40:
1646
+ raise ValueError("Failed to parse header: %r" % header_line)
1647
+ return (tokens[0], tokens[1], int(tokens[2]))
1648
+
1649
+ def _prepare_ref(self, ref: AnyStr) -> bytes:
1650
+ # Required for command to separate refs on stdin, as bytes.
1651
+ if isinstance(ref, bytes):
1652
+ # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text.
1653
+ refstr: str = ref.decode("ascii")
1654
+ elif not isinstance(ref, str):
1655
+ refstr = str(ref) # Could be ref-object.
1656
+ else:
1657
+ refstr = ref
1658
+
1659
+ if not refstr.endswith("\n"):
1660
+ refstr += "\n"
1661
+ return refstr.encode(defenc)
1662
+
1663
+ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt":
1664
+ cur_val = getattr(self, attr_name)
1665
+ if cur_val is not None:
1666
+ return cur_val
1667
+
1668
+ options = {"istream": PIPE, "as_process": True}
1669
+ options.update(kwargs)
1670
+
1671
+ cmd = self._call_process(cmd_name, *args, **options)
1672
+ setattr(self, attr_name, cmd)
1673
+ cmd = cast("Git.AutoInterrupt", cmd)
1674
+ return cmd
1675
+
1676
+ def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]:
1677
+ if cmd.stdin and cmd.stdout:
1678
+ cmd.stdin.write(self._prepare_ref(ref))
1679
+ cmd.stdin.flush()
1680
+ return self._parse_object_header(cmd.stdout.readline())
1681
+ else:
1682
+ raise ValueError("cmd stdin was empty")
1683
+
1684
+ def get_object_header(self, ref: str) -> Tuple[str, str, int]:
1685
+ """Use this method to quickly examine the type and size of the object behind the
1686
+ given ref.
1687
+
1688
+ :note:
1689
+ The method will only suffer from the costs of command invocation once and
1690
+ reuses the command in subsequent calls.
1691
+
1692
+ :return:
1693
+ (hexsha, type_string, size_as_int)
1694
+ """
1695
+ cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
1696
+ return self.__get_object_header(cmd, ref)
1697
+
1698
+ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
1699
+ """Similar to :meth:`get_object_header`, but returns object data as well.
1700
+
1701
+ :return:
1702
+ (hexsha, type_string, size_as_int, data_string)
1703
+
1704
+ :note:
1705
+ Not threadsafe.
1706
+ """
1707
+ hexsha, typename, size, stream = self.stream_object_data(ref)
1708
+ data = stream.read(size)
1709
+ del stream
1710
+ return (hexsha, typename, size, data)
1711
+
1712
+ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
1713
+ """Similar to :meth:`get_object_data`, but returns the data as a stream.
1714
+
1715
+ :return:
1716
+ (hexsha, type_string, size_as_int, stream)
1717
+
1718
+ :note:
1719
+ This method is not threadsafe. You need one independent :class:`Git`
1720
+ instance per thread to be safe!
1721
+ """
1722
+ cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
1723
+ hexsha, typename, size = self.__get_object_header(cmd, ref)
1724
+ cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()
1725
+ return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout))
1726
+
1727
+ def clear_cache(self) -> "Git":
1728
+ """Clear all kinds of internal caches to release resources.
1729
+
1730
+ Currently persistent commands will be interrupted.
1731
+
1732
+ :return:
1733
+ self
1734
+ """
1735
+ for cmd in (self.cat_file_all, self.cat_file_header):
1736
+ if cmd:
1737
+ cmd.__del__()
1738
+
1739
+ self.cat_file_all = None
1740
+ self.cat_file_header = None
1741
+ return self
.venv/lib/python3.11/site-packages/git/compat.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Utilities to help provide compatibility with Python 3.
7
+
8
+ This module exists for historical reasons. Code outside GitPython may make use of public
9
+ members of this module, but is unlikely to benefit from doing so. GitPython continues to
10
+ use some of these utilities, in some cases for compatibility across different platforms.
11
+ """
12
+
13
+ import locale
14
+ import os
15
+ import sys
16
+ import warnings
17
+
18
+ from gitdb.utils.encoding import force_bytes, force_text # noqa: F401
19
+
20
+ # typing --------------------------------------------------------------------
21
+
22
+ from typing import (
23
+ Any, # noqa: F401
24
+ AnyStr,
25
+ Dict, # noqa: F401
26
+ IO, # noqa: F401
27
+ List,
28
+ Optional,
29
+ TYPE_CHECKING,
30
+ Tuple, # noqa: F401
31
+ Type, # noqa: F401
32
+ Union,
33
+ overload,
34
+ )
35
+
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ _deprecated_platform_aliases = {
40
+ "is_win": os.name == "nt",
41
+ "is_posix": os.name == "posix",
42
+ "is_darwin": sys.platform == "darwin",
43
+ }
44
+
45
+
46
+ def _getattr(name: str) -> Any:
47
+ try:
48
+ value = _deprecated_platform_aliases[name]
49
+ except KeyError:
50
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
51
+
52
+ warnings.warn(
53
+ f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
54
+ "Write the desired os.name or sys.platform check explicitly instead.",
55
+ DeprecationWarning,
56
+ stacklevel=2,
57
+ )
58
+ return value
59
+
60
+
61
+ if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
62
+ __getattr__ = _getattr
63
+
64
+
65
+ def __dir__() -> List[str]:
66
+ return [*globals(), *_deprecated_platform_aliases]
67
+
68
+
69
+ is_win: bool
70
+ """Deprecated alias for ``os.name == "nt"`` to check for native Windows.
71
+
72
+ This is deprecated because it is clearer to write out :attr:`os.name` or
73
+ :attr:`sys.platform` checks explicitly, especially in cases where it matters which is
74
+ used.
75
+
76
+ :note:
77
+ ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To detect
78
+ Cygwin, use ``sys.platform == "cygwin"``.
79
+ """
80
+
81
+ is_posix: bool
82
+ """Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
83
+
84
+ This is deprecated because it clearer to write out :attr:`os.name` or
85
+ :attr:`sys.platform` checks explicitly, especially in cases where it matters which is
86
+ used.
87
+
88
+ :note:
89
+ For POSIX systems, more detailed information is available in :attr:`sys.platform`,
90
+ while :attr:`os.name` is always ``"posix"`` on such systems, including macOS
91
+ (Darwin).
92
+ """
93
+
94
+ is_darwin: bool
95
+ """Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
96
+
97
+ This is deprecated because it clearer to write out :attr:`os.name` or
98
+ :attr:`sys.platform` checks explicitly.
99
+
100
+ :note:
101
+ For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
102
+ ``sys.platform == "darwin"``.
103
+ """
104
+
105
+ defenc = sys.getfilesystemencoding()
106
+ """The encoding used to convert between Unicode and bytes filenames."""
107
+
108
+
109
+ @overload
110
+ def safe_decode(s: None) -> None: ...
111
+
112
+
113
+ @overload
114
+ def safe_decode(s: AnyStr) -> str: ...
115
+
116
+
117
+ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]:
118
+ """Safely decode a binary string to Unicode."""
119
+ if isinstance(s, str):
120
+ return s
121
+ elif isinstance(s, bytes):
122
+ return s.decode(defenc, "surrogateescape")
123
+ elif s is None:
124
+ return None
125
+ else:
126
+ raise TypeError("Expected bytes or text, but got %r" % (s,))
127
+
128
+
129
+ @overload
130
+ def safe_encode(s: None) -> None: ...
131
+
132
+
133
+ @overload
134
+ def safe_encode(s: AnyStr) -> bytes: ...
135
+
136
+
137
+ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]:
138
+ """Safely encode a binary string to Unicode."""
139
+ if isinstance(s, str):
140
+ return s.encode(defenc)
141
+ elif isinstance(s, bytes):
142
+ return s
143
+ elif s is None:
144
+ return None
145
+ else:
146
+ raise TypeError("Expected bytes or text, but got %r" % (s,))
147
+
148
+
149
+ @overload
150
+ def win_encode(s: None) -> None: ...
151
+
152
+
153
+ @overload
154
+ def win_encode(s: AnyStr) -> bytes: ...
155
+
156
+
157
+ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]:
158
+ """Encode Unicode strings for process arguments on Windows."""
159
+ if isinstance(s, str):
160
+ return s.encode(locale.getpreferredencoding(False))
161
+ elif isinstance(s, bytes):
162
+ return s
163
+ elif s is not None:
164
+ raise TypeError("Expected bytes or text, but got %r" % (s,))
165
+ return None
.venv/lib/python3.11/site-packages/git/config.py ADDED
@@ -0,0 +1,951 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Parser for reading and writing configuration files."""
7
+
8
+ __all__ = ["GitConfigParser", "SectionConstraint"]
9
+
10
+ import abc
11
+ import configparser as cp
12
+ import fnmatch
13
+ from functools import wraps
14
+ import inspect
15
+ from io import BufferedReader, IOBase
16
+ import logging
17
+ import os
18
+ import os.path as osp
19
+ import re
20
+ import sys
21
+
22
+ from git.compat import defenc, force_text
23
+ from git.util import LockFile
24
+
25
+ # typing-------------------------------------------------------
26
+
27
+ from typing import (
28
+ Any,
29
+ Callable,
30
+ Generic,
31
+ IO,
32
+ List,
33
+ Dict,
34
+ Sequence,
35
+ TYPE_CHECKING,
36
+ Tuple,
37
+ TypeVar,
38
+ Union,
39
+ cast,
40
+ )
41
+
42
+ from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T
43
+
44
+ if TYPE_CHECKING:
45
+ from io import BytesIO
46
+
47
+ from git.repo.base import Repo
48
+
49
+ T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser")
50
+ T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool)
51
+
52
+ if sys.version_info[:3] < (3, 7, 2):
53
+ # typing.Ordereddict not added until Python 3.7.2.
54
+ from collections import OrderedDict
55
+
56
+ OrderedDict_OMD = OrderedDict
57
+ else:
58
+ from typing import OrderedDict
59
+
60
+ OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc]
61
+
62
+ # -------------------------------------------------------------
63
+
64
+ _logger = logging.getLogger(__name__)
65
+
66
+ CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
67
+ """The configuration level of a configuration file."""
68
+
69
+ CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
70
+ """Section pattern to detect conditional includes.
71
+
72
+ See: https://git-scm.com/docs/git-config#_conditional_includes
73
+ """
74
+
75
+
76
+ class MetaParserBuilder(abc.ABCMeta): # noqa: B024
77
+ """Utility class wrapping base-class methods into decorators that assure read-only
78
+ properties."""
79
+
80
+ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder":
81
+ """Equip all base-class methods with a needs_values decorator, and all non-const
82
+ methods with a :func:`set_dirty_and_flush_changes` decorator in addition to
83
+ that.
84
+ """
85
+ kmm = "_mutating_methods_"
86
+ if kmm in clsdict:
87
+ mutating_methods = clsdict[kmm]
88
+ for base in bases:
89
+ methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_"))
90
+ for method_name, method in methods:
91
+ if method_name in clsdict:
92
+ continue
93
+ method_with_values = needs_values(method)
94
+ if method_name in mutating_methods:
95
+ method_with_values = set_dirty_and_flush_changes(method_with_values)
96
+ # END mutating methods handling
97
+
98
+ clsdict[method_name] = method_with_values
99
+ # END for each name/method pair
100
+ # END for each base
101
+ # END if mutating methods configuration is set
102
+
103
+ new_type = super().__new__(cls, name, bases, clsdict)
104
+ return new_type
105
+
106
+
107
+ def needs_values(func: Callable[..., _T]) -> Callable[..., _T]:
108
+ """Return a method for ensuring we read values (on demand) before we try to access
109
+ them."""
110
+
111
+ @wraps(func)
112
+ def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
113
+ self.read()
114
+ return func(self, *args, **kwargs)
115
+
116
+ # END wrapper method
117
+ return assure_data_present
118
+
119
+
120
+ def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]:
121
+ """Return a method that checks whether given non constant function may be called.
122
+
123
+ If so, the instance will be set dirty. Additionally, we flush the changes right to
124
+ disk.
125
+ """
126
+
127
+ def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
128
+ rval = non_const_func(self, *args, **kwargs)
129
+ self._dirty = True
130
+ self.write()
131
+ return rval
132
+
133
+ # END wrapper method
134
+ flush_changes.__name__ = non_const_func.__name__
135
+ return flush_changes
136
+
137
+
138
+ class SectionConstraint(Generic[T_ConfigParser]):
139
+ """Constrains a ConfigParser to only option commands which are constrained to
140
+ always use the section we have been initialized with.
141
+
142
+ It supports all ConfigParser methods that operate on an option.
143
+
144
+ :note:
145
+ If used as a context manager, will release the wrapped ConfigParser.
146
+ """
147
+
148
+ __slots__ = ("_config", "_section_name")
149
+
150
+ _valid_attrs_ = (
151
+ "get_value",
152
+ "set_value",
153
+ "get",
154
+ "set",
155
+ "getint",
156
+ "getfloat",
157
+ "getboolean",
158
+ "has_option",
159
+ "remove_section",
160
+ "remove_option",
161
+ "options",
162
+ )
163
+
164
+ def __init__(self, config: T_ConfigParser, section: str) -> None:
165
+ self._config = config
166
+ self._section_name = section
167
+
168
+ def __del__(self) -> None:
169
+ # Yes, for some reason, we have to call it explicitly for it to work in PY3 !
170
+ # Apparently __del__ doesn't get call anymore if refcount becomes 0
171
+ # Ridiculous ... .
172
+ self._config.release()
173
+
174
+ def __getattr__(self, attr: str) -> Any:
175
+ if attr in self._valid_attrs_:
176
+ return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs)
177
+ return super().__getattribute__(attr)
178
+
179
+ def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any:
180
+ """Call the configuration at the given method which must take a section name as
181
+ first argument."""
182
+ return getattr(self._config, method)(self._section_name, *args, **kwargs)
183
+
184
+ @property
185
+ def config(self) -> T_ConfigParser:
186
+ """return: ConfigParser instance we constrain"""
187
+ return self._config
188
+
189
+ def release(self) -> None:
190
+ """Equivalent to :meth:`GitConfigParser.release`, which is called on our
191
+ underlying parser instance."""
192
+ return self._config.release()
193
+
194
+ def __enter__(self) -> "SectionConstraint[T_ConfigParser]":
195
+ self._config.__enter__()
196
+ return self
197
+
198
+ def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None:
199
+ self._config.__exit__(exception_type, exception_value, traceback)
200
+
201
+
202
+ class _OMD(OrderedDict_OMD):
203
+ """Ordered multi-dict."""
204
+
205
+ def __setitem__(self, key: str, value: _T) -> None:
206
+ super().__setitem__(key, [value])
207
+
208
+ def add(self, key: str, value: Any) -> None:
209
+ if key not in self:
210
+ super().__setitem__(key, [value])
211
+ return
212
+
213
+ super().__getitem__(key).append(value)
214
+
215
+ def setall(self, key: str, values: List[_T]) -> None:
216
+ super().__setitem__(key, values)
217
+
218
+ def __getitem__(self, key: str) -> Any:
219
+ return super().__getitem__(key)[-1]
220
+
221
+ def getlast(self, key: str) -> Any:
222
+ return super().__getitem__(key)[-1]
223
+
224
+ def setlast(self, key: str, value: Any) -> None:
225
+ if key not in self:
226
+ super().__setitem__(key, [value])
227
+ return
228
+
229
+ prior = super().__getitem__(key)
230
+ prior[-1] = value
231
+
232
+ def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]:
233
+ return super().get(key, [default])[-1]
234
+
235
+ def getall(self, key: str) -> List[_T]:
236
+ return super().__getitem__(key)
237
+
238
+ def items(self) -> List[Tuple[str, _T]]: # type: ignore[override]
239
+ """List of (key, last value for key)."""
240
+ return [(k, self[k]) for k in self]
241
+
242
+ def items_all(self) -> List[Tuple[str, List[_T]]]:
243
+ """List of (key, list of values for key)."""
244
+ return [(k, self.getall(k)) for k in self]
245
+
246
+
247
+ def get_config_path(config_level: Lit_config_levels) -> str:
248
+ # We do not support an absolute path of the gitconfig on Windows.
249
+ # Use the global config instead.
250
+ if sys.platform == "win32" and config_level == "system":
251
+ config_level = "global"
252
+
253
+ if config_level == "system":
254
+ return "/etc/gitconfig"
255
+ elif config_level == "user":
256
+ config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
257
+ return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
258
+ elif config_level == "global":
259
+ return osp.normpath(osp.expanduser("~/.gitconfig"))
260
+ elif config_level == "repository":
261
+ raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path")
262
+ else:
263
+ # Should not reach here. Will raise ValueError if does. Static typing will warn
264
+ # about missing elifs.
265
+ assert_never( # type: ignore[unreachable]
266
+ config_level,
267
+ ValueError(f"Invalid configuration level: {config_level!r}"),
268
+ )
269
+
270
+
271
+ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
272
+ """Implements specifics required to read git style configuration files.
273
+
274
+ This variation behaves much like the :manpage:`git-config(1)` command, such that the
275
+ configuration will be read on demand based on the filepath given during
276
+ initialization.
277
+
278
+ The changes will automatically be written once the instance goes out of scope, but
279
+ can be triggered manually as well.
280
+
281
+ The configuration file will be locked if you intend to change values preventing
282
+ other instances to write concurrently.
283
+
284
+ :note:
285
+ The config is case-sensitive even when queried, hence section and option names
286
+ must match perfectly.
287
+
288
+ :note:
289
+ If used as a context manager, this will release the locked file.
290
+ """
291
+
292
+ # { Configuration
293
+ t_lock = LockFile
294
+ """The lock type determines the type of lock to use in new configuration readers.
295
+
296
+ They must be compatible to the :class:`~git.util.LockFile` interface.
297
+ A suitable alternative would be the :class:`~git.util.BlockingLockFile`.
298
+ """
299
+
300
+ re_comment = re.compile(r"^\s*[#;]")
301
+ # } END configuration
302
+
303
+ optvalueonly_source = r"\s*(?P<option>[^:=\s][^:=]*)"
304
+
305
+ OPTVALUEONLY = re.compile(optvalueonly_source)
306
+
307
+ OPTCRE = re.compile(optvalueonly_source + r"\s*(?P<vi>[:=])\s*" + r"(?P<value>.*)$")
308
+
309
+ del optvalueonly_source
310
+
311
+ _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
312
+ """Names of :class:`~configparser.RawConfigParser` methods able to change the
313
+ instance."""
314
+
315
+ def __init__(
316
+ self,
317
+ file_or_files: Union[None, PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = None,
318
+ read_only: bool = True,
319
+ merge_includes: bool = True,
320
+ config_level: Union[Lit_config_levels, None] = None,
321
+ repo: Union["Repo", None] = None,
322
+ ) -> None:
323
+ """Initialize a configuration reader to read the given `file_or_files` and to
324
+ possibly allow changes to it by setting `read_only` False.
325
+
326
+ :param file_or_files:
327
+ A file path or file object, or a sequence of possibly more than one of them.
328
+
329
+ :param read_only:
330
+ If ``True``, the ConfigParser may only read the data, but not change it.
331
+ If ``False``, only a single file path or file object may be given. We will
332
+ write back the changes when they happen, or when the ConfigParser is
333
+ released. This will not happen if other configuration files have been
334
+ included.
335
+
336
+ :param merge_includes:
337
+ If ``True``, we will read files mentioned in ``[include]`` sections and
338
+ merge their contents into ours. This makes it impossible to write back an
339
+ individual configuration file. Thus, if you want to modify a single
340
+ configuration file, turn this off to leave the original dataset unaltered
341
+ when reading it.
342
+
343
+ :param repo:
344
+ Reference to repository to use if ``[includeIf]`` sections are found in
345
+ configuration files.
346
+ """
347
+ cp.RawConfigParser.__init__(self, dict_type=_OMD)
348
+ self._dict: Callable[..., _OMD]
349
+ self._defaults: _OMD
350
+ self._sections: _OMD
351
+
352
+ # Used in Python 3. Needs to stay in sync with sections for underlying
353
+ # implementation to work.
354
+ if not hasattr(self, "_proxies"):
355
+ self._proxies = self._dict()
356
+
357
+ if file_or_files is not None:
358
+ self._file_or_files: Union[PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = file_or_files
359
+ else:
360
+ if config_level is None:
361
+ if read_only:
362
+ self._file_or_files = [
363
+ get_config_path(cast(Lit_config_levels, f)) for f in CONFIG_LEVELS if f != "repository"
364
+ ]
365
+ else:
366
+ raise ValueError("No configuration level or configuration files specified")
367
+ else:
368
+ self._file_or_files = [get_config_path(config_level)]
369
+
370
+ self._read_only = read_only
371
+ self._dirty = False
372
+ self._is_initialized = False
373
+ self._merge_includes = merge_includes
374
+ self._repo = repo
375
+ self._lock: Union["LockFile", None] = None
376
+ self._acquire_lock()
377
+
378
+ def _acquire_lock(self) -> None:
379
+ if not self._read_only:
380
+ if not self._lock:
381
+ if isinstance(self._file_or_files, (str, os.PathLike)):
382
+ file_or_files = self._file_or_files
383
+ elif isinstance(self._file_or_files, (tuple, list, Sequence)):
384
+ raise ValueError(
385
+ "Write-ConfigParsers can operate on a single file only, multiple files have been passed"
386
+ )
387
+ else:
388
+ file_or_files = self._file_or_files.name
389
+
390
+ # END get filename from handle/stream
391
+ # Initialize lock base - we want to write.
392
+ self._lock = self.t_lock(file_or_files)
393
+ # END lock check
394
+
395
+ self._lock._obtain_lock()
396
+ # END read-only check
397
+
398
+ def __del__(self) -> None:
399
+ """Write pending changes if required and release locks."""
400
+ # NOTE: Only consistent in Python 2.
401
+ self.release()
402
+
403
+ def __enter__(self) -> "GitConfigParser":
404
+ self._acquire_lock()
405
+ return self
406
+
407
+ def __exit__(self, *args: Any) -> None:
408
+ self.release()
409
+
410
+ def release(self) -> None:
411
+ """Flush changes and release the configuration write lock. This instance must
412
+ not be used anymore afterwards.
413
+
414
+ In Python 3, it's required to explicitly release locks and flush changes, as
415
+ ``__del__`` is not called deterministically anymore.
416
+ """
417
+ # Checking for the lock here makes sure we do not raise during write()
418
+ # in case an invalid parser was created who could not get a lock.
419
+ if self.read_only or (self._lock and not self._lock._has_lock()):
420
+ return
421
+
422
+ try:
423
+ self.write()
424
+ except IOError:
425
+ _logger.error("Exception during destruction of GitConfigParser", exc_info=True)
426
+ except ReferenceError:
427
+ # This happens in Python 3... and usually means that some state cannot be
428
+ # written as the sections dict cannot be iterated. This usually happens when
429
+ # the interpreter is shutting down. Can it be fixed?
430
+ pass
431
+ finally:
432
+ if self._lock is not None:
433
+ self._lock._release_lock()
434
+
435
+ def optionxform(self, optionstr: str) -> str:
436
+ """Do not transform options in any way when writing."""
437
+ return optionstr
438
+
439
+ def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
440
+ """Originally a direct copy of the Python 2.4 version of
441
+ :meth:`RawConfigParser._read <configparser.RawConfigParser._read>`, to ensure it
442
+ uses ordered dicts.
443
+
444
+ The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since
445
+ Python 3.7. This has some other changes, especially that it ignores initial
446
+ whitespace, since git uses tabs. (Big comments are removed to be more compact.)
447
+ """
448
+ cursect = None # None, or a dictionary.
449
+ optname = None
450
+ lineno = 0
451
+ is_multi_line = False
452
+ e = None # None, or an exception.
453
+
454
+ def string_decode(v: str) -> str:
455
+ if v and v.endswith("\\"):
456
+ v = v[:-1]
457
+ # END cut trailing escapes to prevent decode error
458
+
459
+ return v.encode(defenc).decode("unicode_escape")
460
+
461
+ # END string_decode
462
+
463
+ while True:
464
+ # We assume to read binary!
465
+ line = fp.readline().decode(defenc)
466
+ if not line:
467
+ break
468
+ lineno = lineno + 1
469
+ # Comment or blank line?
470
+ if line.strip() == "" or self.re_comment.match(line):
471
+ continue
472
+ if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR":
473
+ # No leading whitespace.
474
+ continue
475
+
476
+ # Is it a section header?
477
+ mo = self.SECTCRE.match(line.strip())
478
+ if not is_multi_line and mo:
479
+ sectname: str = mo.group("header").strip()
480
+ if sectname in self._sections:
481
+ cursect = self._sections[sectname]
482
+ elif sectname == cp.DEFAULTSECT:
483
+ cursect = self._defaults
484
+ else:
485
+ cursect = self._dict((("__name__", sectname),))
486
+ self._sections[sectname] = cursect
487
+ self._proxies[sectname] = None
488
+ # So sections can't start with a continuation line.
489
+ optname = None
490
+ # No section header in the file?
491
+ elif cursect is None:
492
+ raise cp.MissingSectionHeaderError(fpname, lineno, line)
493
+ # An option line?
494
+ elif not is_multi_line:
495
+ mo = self.OPTCRE.match(line)
496
+ if mo:
497
+ # We might just have handled the last line, which could contain a quotation we want to remove.
498
+ optname, vi, optval = mo.group("option", "vi", "value")
499
+ optname = self.optionxform(optname.rstrip())
500
+
501
+ if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
502
+ pos = optval.find(";")
503
+ if pos != -1 and optval[pos - 1].isspace():
504
+ optval = optval[:pos]
505
+ optval = optval.strip()
506
+
507
+ if len(optval) < 2 or optval[0] != '"':
508
+ # Does not open quoting.
509
+ pass
510
+ elif optval[-1] != '"':
511
+ # Opens quoting and does not close: appears to start multi-line quoting.
512
+ is_multi_line = True
513
+ optval = string_decode(optval[1:])
514
+ elif optval.find("\\", 1, -1) == -1 and optval.find('"', 1, -1) == -1:
515
+ # Opens and closes quoting. Single line, and all we need is quote removal.
516
+ optval = optval[1:-1]
517
+ # TODO: Handle other quoted content, especially well-formed backslash escapes.
518
+
519
+ # Preserves multiple values for duplicate optnames.
520
+ cursect.add(optname, optval)
521
+ else:
522
+ # Check if it's an option with no value - it's just ignored by git.
523
+ if not self.OPTVALUEONLY.match(line):
524
+ if not e:
525
+ e = cp.ParsingError(fpname)
526
+ e.append(lineno, repr(line))
527
+ continue
528
+ else:
529
+ line = line.rstrip()
530
+ if line.endswith('"'):
531
+ is_multi_line = False
532
+ line = line[:-1]
533
+ # END handle quotations
534
+ optval = cursect.getlast(optname)
535
+ cursect.setlast(optname, optval + string_decode(line))
536
+ # END parse section or option
537
+ # END while reading
538
+
539
+ # If any parsing errors occurred, raise an exception.
540
+ if e:
541
+ raise e
542
+
543
+ def _has_includes(self) -> Union[bool, int]:
544
+ return self._merge_includes and len(self._included_paths())
545
+
546
+ def _included_paths(self) -> List[Tuple[str, str]]:
547
+ """List all paths that must be included to configuration.
548
+
549
+ :return:
550
+ The list of paths, where each path is a tuple of (option, value).
551
+ """
552
+ paths = []
553
+
554
+ for section in self.sections():
555
+ if section == "include":
556
+ paths += self.items(section)
557
+
558
+ match = CONDITIONAL_INCLUDE_REGEXP.search(section)
559
+ if match is None or self._repo is None:
560
+ continue
561
+
562
+ keyword = match.group(1)
563
+ value = match.group(2).strip()
564
+
565
+ if keyword in ["gitdir", "gitdir/i"]:
566
+ value = osp.expanduser(value)
567
+
568
+ if not any(value.startswith(s) for s in ["./", "/"]):
569
+ value = "**/" + value
570
+ if value.endswith("/"):
571
+ value += "**"
572
+
573
+ # Ensure that glob is always case insensitive if required.
574
+ if keyword.endswith("/i"):
575
+ value = re.sub(
576
+ r"[a-zA-Z]",
577
+ lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()),
578
+ value,
579
+ )
580
+ if self._repo.git_dir:
581
+ if fnmatch.fnmatchcase(str(self._repo.git_dir), value):
582
+ paths += self.items(section)
583
+
584
+ elif keyword == "onbranch":
585
+ try:
586
+ branch_name = self._repo.active_branch.name
587
+ except TypeError:
588
+ # Ignore section if active branch cannot be retrieved.
589
+ continue
590
+
591
+ if fnmatch.fnmatchcase(branch_name, value):
592
+ paths += self.items(section)
593
+
594
+ return paths
595
+
596
+ def read(self) -> None: # type: ignore[override]
597
+ """Read the data stored in the files we have been initialized with.
598
+
599
+ This will ignore files that cannot be read, possibly leaving an empty
600
+ configuration.
601
+
602
+ :raise IOError:
603
+ If a file cannot be handled.
604
+ """
605
+ if self._is_initialized:
606
+ return
607
+ self._is_initialized = True
608
+
609
+ files_to_read: List[Union[PathLike, IO]] = [""]
610
+ if isinstance(self._file_or_files, (str, os.PathLike)):
611
+ # For str or Path, as str is a type of Sequence.
612
+ files_to_read = [self._file_or_files]
613
+ elif not isinstance(self._file_or_files, (tuple, list, Sequence)):
614
+ # Could merge with above isinstance once runtime type known.
615
+ files_to_read = [self._file_or_files]
616
+ else: # For lists or tuples.
617
+ files_to_read = list(self._file_or_files)
618
+ # END ensure we have a copy of the paths to handle
619
+
620
+ seen = set(files_to_read)
621
+ num_read_include_files = 0
622
+ while files_to_read:
623
+ file_path = files_to_read.pop(0)
624
+ file_ok = False
625
+
626
+ if hasattr(file_path, "seek"):
627
+ # Must be a file-object.
628
+ # TODO: Replace cast with assert to narrow type, once sure.
629
+ file_path = cast(IO[bytes], file_path)
630
+ self._read(file_path, file_path.name)
631
+ else:
632
+ # Assume a path if it is not a file-object.
633
+ file_path = cast(PathLike, file_path)
634
+ try:
635
+ with open(file_path, "rb") as fp:
636
+ file_ok = True
637
+ self._read(fp, fp.name)
638
+ except IOError:
639
+ continue
640
+
641
+ # Read includes and append those that we didn't handle yet. We expect all
642
+ # paths to be normalized and absolute (and will ensure that is the case).
643
+ if self._has_includes():
644
+ for _, include_path in self._included_paths():
645
+ if include_path.startswith("~"):
646
+ include_path = osp.expanduser(include_path)
647
+ if not osp.isabs(include_path):
648
+ if not file_ok:
649
+ continue
650
+ # END ignore relative paths if we don't know the configuration file path
651
+ file_path = cast(PathLike, file_path)
652
+ assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
653
+ include_path = osp.join(osp.dirname(file_path), include_path)
654
+ # END make include path absolute
655
+ include_path = osp.normpath(include_path)
656
+ if include_path in seen or not os.access(include_path, os.R_OK):
657
+ continue
658
+ seen.add(include_path)
659
+ # Insert included file to the top to be considered first.
660
+ files_to_read.insert(0, include_path)
661
+ num_read_include_files += 1
662
+ # END each include path in configuration file
663
+ # END handle includes
664
+ # END for each file object to read
665
+
666
+ # If there was no file included, we can safely write back (potentially) the
667
+ # configuration file without altering its meaning.
668
+ if num_read_include_files == 0:
669
+ self._merge_includes = False
670
+
671
+ def _write(self, fp: IO) -> None:
672
+ """Write an .ini-format representation of the configuration state in
673
+ git compatible format."""
674
+
675
+ def write_section(name: str, section_dict: _OMD) -> None:
676
+ fp.write(("[%s]\n" % name).encode(defenc))
677
+
678
+ values: Sequence[str] # Runtime only gets str in tests, but should be whatever _OMD stores.
679
+ v: str
680
+ for key, values in section_dict.items_all():
681
+ if key == "__name__":
682
+ continue
683
+
684
+ for v in values:
685
+ fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
686
+ # END if key is not __name__
687
+
688
+ # END section writing
689
+
690
+ if self._defaults:
691
+ write_section(cp.DEFAULTSECT, self._defaults)
692
+ value: _OMD
693
+
694
+ for name, value in self._sections.items():
695
+ write_section(name, value)
696
+
697
+ def items(self, section_name: str) -> List[Tuple[str, str]]: # type: ignore[override]
698
+ """:return: list((option, value), ...) pairs of all items in the given section"""
699
+ return [(k, v) for k, v in super().items(section_name) if k != "__name__"]
700
+
701
+ def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
702
+ """:return: list((option, [values...]), ...) pairs of all items in the given section"""
703
+ rv = _OMD(self._defaults)
704
+
705
+ for k, vs in self._sections[section_name].items_all():
706
+ if k == "__name__":
707
+ continue
708
+
709
+ if k in rv and rv.getall(k) == vs:
710
+ continue
711
+
712
+ for v in vs:
713
+ rv.add(k, v)
714
+
715
+ return rv.items_all()
716
+
717
+ @needs_values
718
+ def write(self) -> None:
719
+ """Write changes to our file, if there are changes at all.
720
+
721
+ :raise IOError:
722
+ If this is a read-only writer instance or if we could not obtain a file
723
+ lock.
724
+ """
725
+ self._assure_writable("write")
726
+ if not self._dirty:
727
+ return
728
+
729
+ if isinstance(self._file_or_files, (list, tuple)):
730
+ raise AssertionError(
731
+ "Cannot write back if there is not exactly a single file to write to, have %i files"
732
+ % len(self._file_or_files)
733
+ )
734
+ # END assert multiple files
735
+
736
+ if self._has_includes():
737
+ _logger.debug(
738
+ "Skipping write-back of configuration file as include files were merged in."
739
+ + "Set merge_includes=False to prevent this."
740
+ )
741
+ return
742
+ # END stop if we have include files
743
+
744
+ fp = self._file_or_files
745
+
746
+ # We have a physical file on disk, so get a lock.
747
+ is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # TODO: Use PathLike (having dropped 3.5).
748
+ if is_file_lock and self._lock is not None: # Else raise error?
749
+ self._lock._obtain_lock()
750
+
751
+ if not hasattr(fp, "seek"):
752
+ fp = cast(PathLike, fp)
753
+ with open(fp, "wb") as fp_open:
754
+ self._write(fp_open)
755
+ else:
756
+ fp = cast("BytesIO", fp)
757
+ fp.seek(0)
758
+ # Make sure we do not overwrite into an existing file.
759
+ if hasattr(fp, "truncate"):
760
+ fp.truncate()
761
+ self._write(fp)
762
+
763
+ def _assure_writable(self, method_name: str) -> None:
764
+ if self.read_only:
765
+ raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
766
+
767
+ def add_section(self, section: str) -> None:
768
+ """Assures added options will stay in order."""
769
+ return super().add_section(section)
770
+
771
+ @property
772
+ def read_only(self) -> bool:
773
+ """:return: ``True`` if this instance may change the configuration file"""
774
+ return self._read_only
775
+
776
+ # FIXME: Figure out if default or return type can really include bool.
777
+ def get_value(
778
+ self,
779
+ section: str,
780
+ option: str,
781
+ default: Union[int, float, str, bool, None] = None,
782
+ ) -> Union[int, float, str, bool]:
783
+ """Get an option's value.
784
+
785
+ If multiple values are specified for this option in the section, the last one
786
+ specified is returned.
787
+
788
+ :param default:
789
+ If not ``None``, the given default value will be returned in case the option
790
+ did not exist.
791
+
792
+ :return:
793
+ A properly typed value, either int, float or string
794
+
795
+ :raise TypeError:
796
+ In case the value could not be understood.
797
+ Otherwise the exceptions known to the ConfigParser will be raised.
798
+ """
799
+ try:
800
+ valuestr = self.get(section, option)
801
+ except Exception:
802
+ if default is not None:
803
+ return default
804
+ raise
805
+
806
+ return self._string_to_value(valuestr)
807
+
808
+ def get_values(
809
+ self,
810
+ section: str,
811
+ option: str,
812
+ default: Union[int, float, str, bool, None] = None,
813
+ ) -> List[Union[int, float, str, bool]]:
814
+ """Get an option's values.
815
+
816
+ If multiple values are specified for this option in the section, all are
817
+ returned.
818
+
819
+ :param default:
820
+ If not ``None``, a list containing the given default value will be returned
821
+ in case the option did not exist.
822
+
823
+ :return:
824
+ A list of properly typed values, either int, float or string
825
+
826
+ :raise TypeError:
827
+ In case the value could not be understood.
828
+ Otherwise the exceptions known to the ConfigParser will be raised.
829
+ """
830
+ try:
831
+ self.sections()
832
+ lst = self._sections[section].getall(option)
833
+ except Exception:
834
+ if default is not None:
835
+ return [default]
836
+ raise
837
+
838
+ return [self._string_to_value(valuestr) for valuestr in lst]
839
+
840
+ def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]:
841
+ types = (int, float)
842
+ for numtype in types:
843
+ try:
844
+ val = numtype(valuestr)
845
+ # truncated value ?
846
+ if val != float(valuestr):
847
+ continue
848
+ return val
849
+ except (ValueError, TypeError):
850
+ continue
851
+ # END for each numeric type
852
+
853
+ # Try boolean values as git uses them.
854
+ vl = valuestr.lower()
855
+ if vl == "false":
856
+ return False
857
+ if vl == "true":
858
+ return True
859
+
860
+ if not isinstance(valuestr, str):
861
+ raise TypeError(
862
+ "Invalid value type: only int, long, float and str are allowed",
863
+ valuestr,
864
+ )
865
+
866
+ return valuestr
867
+
868
+ def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str:
869
+ if isinstance(value, (int, float, bool)):
870
+ return str(value)
871
+ return force_text(value)
872
+
873
+ @needs_values
874
+ @set_dirty_and_flush_changes
875
+ def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
876
+ """Set the given option in section to the given value.
877
+
878
+ This will create the section if required, and will not throw as opposed to the
879
+ default ConfigParser ``set`` method.
880
+
881
+ :param section:
882
+ Name of the section in which the option resides or should reside.
883
+
884
+ :param option:
885
+ Name of the options whose value to set.
886
+
887
+ :param value:
888
+ Value to set the option to. It must be a string or convertible to a string.
889
+
890
+ :return:
891
+ This instance
892
+ """
893
+ if not self.has_section(section):
894
+ self.add_section(section)
895
+ self.set(section, option, self._value_to_string(value))
896
+ return self
897
+
898
+ @needs_values
899
+ @set_dirty_and_flush_changes
900
+ def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
901
+ """Add a value for the given option in section.
902
+
903
+ This will create the section if required, and will not throw as opposed to the
904
+ default ConfigParser ``set`` method. The value becomes the new value of the
905
+ option as returned by :meth:`get_value`, and appends to the list of values
906
+ returned by :meth:`get_values`.
907
+
908
+ :param section:
909
+ Name of the section in which the option resides or should reside.
910
+
911
+ :param option:
912
+ Name of the option.
913
+
914
+ :param value:
915
+ Value to add to option. It must be a string or convertible to a string.
916
+
917
+ :return:
918
+ This instance
919
+ """
920
+ if not self.has_section(section):
921
+ self.add_section(section)
922
+ self._sections[section].add(option, self._value_to_string(value))
923
+ return self
924
+
925
+ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
926
+ """Rename the given section to `new_name`.
927
+
928
+ :raise ValueError:
929
+ If:
930
+
931
+ * `section` doesn't exist.
932
+ * A section with `new_name` does already exist.
933
+
934
+ :return:
935
+ This instance
936
+ """
937
+ if not self.has_section(section):
938
+ raise ValueError("Source section '%s' doesn't exist" % section)
939
+ if self.has_section(new_name):
940
+ raise ValueError("Destination section '%s' already exists" % new_name)
941
+
942
+ super().add_section(new_name)
943
+ new_section = self._sections[new_name]
944
+ for k, vs in self.items_all(section):
945
+ new_section.setall(k, vs)
946
+ # END for each value to copy
947
+
948
+ # This call writes back the changes, which is why we don't have the respective
949
+ # decorator.
950
+ self.remove_section(section)
951
+ return self
.venv/lib/python3.11/site-packages/git/db.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Module with our own gitdb implementation - it uses the git command."""
5
+
6
+ __all__ = ["GitCmdObjectDB", "GitDB"]
7
+
8
+ from gitdb.base import OInfo, OStream
9
+ from gitdb.db import GitDB, LooseObjectDB
10
+ from gitdb.exc import BadObject
11
+
12
+ from git.util import bin_to_hex, hex_to_bin
13
+ from git.exc import GitCommandError
14
+
15
+ # typing-------------------------------------------------
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ from git.types import PathLike
20
+
21
+ if TYPE_CHECKING:
22
+ from git.cmd import Git
23
+
24
+ # --------------------------------------------------------
25
+
26
+
27
+ class GitCmdObjectDB(LooseObjectDB):
28
+ """A database representing the default git object store, which includes loose
29
+ objects, pack files and an alternates file.
30
+
31
+ It will create objects only in the loose object database.
32
+ """
33
+
34
+ def __init__(self, root_path: PathLike, git: "Git") -> None:
35
+ """Initialize this instance with the root and a git command."""
36
+ super().__init__(root_path)
37
+ self._git = git
38
+
39
+ def info(self, binsha: bytes) -> OInfo:
40
+ """Get a git object header (using git itself)."""
41
+ hexsha, typename, size = self._git.get_object_header(bin_to_hex(binsha))
42
+ return OInfo(hex_to_bin(hexsha), typename, size)
43
+
44
+ def stream(self, binsha: bytes) -> OStream:
45
+ """Get git object data as a stream supporting ``read()`` (using git itself)."""
46
+ hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha))
47
+ return OStream(hex_to_bin(hexsha), typename, size, stream)
48
+
49
+ # { Interface
50
+
51
+ def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
52
+ """
53
+ :return:
54
+ Full binary 20 byte sha from the given partial hexsha
55
+
56
+ :raise gitdb.exc.AmbiguousObjectName:
57
+
58
+ :raise gitdb.exc.BadObject:
59
+
60
+ :note:
61
+ Currently we only raise :exc:`~gitdb.exc.BadObject` as git does not
62
+ communicate ambiguous objects separately.
63
+ """
64
+ try:
65
+ hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)
66
+ return hex_to_bin(hexsha)
67
+ except (GitCommandError, ValueError) as e:
68
+ raise BadObject(partial_hexsha) from e
69
+ # END handle exceptions
70
+
71
+ # } END interface
.venv/lib/python3.11/site-packages/git/diff.py ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ __all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"]
7
+
8
+ import enum
9
+ import re
10
+ import warnings
11
+
12
+ from git.cmd import handle_process_output
13
+ from git.compat import defenc
14
+ from git.objects.blob import Blob
15
+ from git.objects.util import mode_str_to_int
16
+ from git.util import finalize_process, hex_to_bin
17
+
18
+ # typing ------------------------------------------------------------------
19
+
20
+ from typing import (
21
+ Any,
22
+ Iterator,
23
+ List,
24
+ Match,
25
+ Optional,
26
+ Tuple,
27
+ TYPE_CHECKING,
28
+ TypeVar,
29
+ Union,
30
+ cast,
31
+ )
32
+ from git.types import Literal, PathLike
33
+
34
+ if TYPE_CHECKING:
35
+ from subprocess import Popen
36
+
37
+ from git.cmd import Git
38
+ from git.objects.base import IndexObject
39
+ from git.objects.commit import Commit
40
+ from git.objects.tree import Tree
41
+ from git.repo.base import Repo
42
+
43
+ Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
44
+
45
+ # ------------------------------------------------------------------------
46
+
47
+
48
+ @enum.unique
49
+ class DiffConstants(enum.Enum):
50
+ """Special objects for :meth:`Diffable.diff`.
51
+
52
+ See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
53
+ values including these.
54
+
55
+ :note:
56
+ These constants are also available as attributes of the :mod:`git.diff` module,
57
+ the :class:`Diffable` class and its subclasses and instances, and the top-level
58
+ :mod:`git` module.
59
+ """
60
+
61
+ NULL_TREE = enum.auto()
62
+ """Stand-in indicating you want to compare against the empty tree in diffs.
63
+
64
+ Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
65
+ :const:`Diffable.NULL_TREE`.
66
+ """
67
+
68
+ INDEX = enum.auto()
69
+ """Stand-in indicating you want to diff against the index.
70
+
71
+ Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
72
+ :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
73
+ kept for backward compatibility and made an alias of this, so it may still be used.
74
+ """
75
+
76
+
77
+ NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
78
+ """Stand-in indicating you want to compare against the empty tree in diffs.
79
+
80
+ See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
81
+
82
+ This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
83
+ :const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
84
+ """
85
+
86
+ INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
87
+ """Stand-in indicating you want to diff against the index.
88
+
89
+ See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
90
+
91
+ This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
92
+ :const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
93
+ """
94
+
95
+ _octal_byte_re = re.compile(rb"\\([0-9]{3})")
96
+
97
+
98
+ def _octal_repl(matchobj: Match) -> bytes:
99
+ value = matchobj.group(1)
100
+ value = int(value, 8)
101
+ value = bytes(bytearray((value,)))
102
+ return value
103
+
104
+
105
+ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
106
+ if path == b"/dev/null":
107
+ return None
108
+
109
+ if path.startswith(b'"') and path.endswith(b'"'):
110
+ path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\")
111
+
112
+ path = _octal_byte_re.sub(_octal_repl, path)
113
+
114
+ if has_ab_prefix:
115
+ assert path.startswith(b"a/") or path.startswith(b"b/")
116
+ path = path[2:]
117
+
118
+ return path
119
+
120
+
121
+ class Diffable:
122
+ """Common interface for all objects that can be diffed against another object of
123
+ compatible type.
124
+
125
+ :note:
126
+ Subclasses require a :attr:`repo` member, as it is the case for
127
+ :class:`~git.objects.base.Object` instances. For practical reasons we do not
128
+ derive from :class:`~git.objects.base.Object`.
129
+ """
130
+
131
+ __slots__ = ()
132
+
133
+ repo: "Repo"
134
+ """Repository to operate on. Must be provided by subclass or sibling class."""
135
+
136
+ NULL_TREE = NULL_TREE
137
+ """Stand-in indicating you want to compare against the empty tree in diffs.
138
+
139
+ See the :meth:`diff` method, which accepts this as a value of its ``other``
140
+ parameter.
141
+
142
+ This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
143
+ :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
144
+ """
145
+
146
+ INDEX = INDEX
147
+ """Stand-in indicating you want to diff against the index.
148
+
149
+ See the :meth:`diff` method, which accepts this as a value of its ``other``
150
+ parameter.
151
+
152
+ This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
153
+ :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
154
+ which is kept for backward compatibility (it is now defined an alias of this).
155
+ """
156
+
157
+ Index = INDEX
158
+ """Stand-in indicating you want to diff against the index
159
+ (same as :const:`~Diffable.INDEX`).
160
+
161
+ This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
162
+ :const:`~Diffable.INDEX` and :meth:`diff` for details.
163
+
164
+ :note:
165
+ Although always meant for use as an opaque constant, this was formerly defined
166
+ as a class. Its usage is unchanged, but static type annotations that attempt
167
+ to permit only this object must be changed to avoid new mypy errors. This was
168
+ previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
169
+ It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
170
+ """
171
+
172
+ def _process_diff_args(
173
+ self,
174
+ args: List[Union[PathLike, "Diffable"]],
175
+ ) -> List[Union[PathLike, "Diffable"]]:
176
+ """
177
+ :return:
178
+ Possibly altered version of the given args list.
179
+ This method is called right before git command execution.
180
+ Subclasses can use it to alter the behaviour of the superclass.
181
+ """
182
+ return args
183
+
184
+ def diff(
185
+ self,
186
+ other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
187
+ paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
188
+ create_patch: bool = False,
189
+ **kwargs: Any,
190
+ ) -> "DiffIndex[Diff]":
191
+ """Create diffs between two items being trees, trees and index or an index and
192
+ the working tree. Detects renames automatically.
193
+
194
+ :param other:
195
+ This the item to compare us with.
196
+
197
+ * If ``None``, we will be compared to the working tree.
198
+
199
+ * If a :class:`~git.types.Tree_ish` or string, it will be compared against
200
+ the respective tree.
201
+
202
+ * If :const:`INDEX`, it will be compared against the index.
203
+
204
+ * If :const:`NULL_TREE`, it will compare against the empty tree.
205
+
206
+ This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
207
+ method will not by default fail on bare repositories.
208
+
209
+ :param paths:
210
+ This a list of paths or a single path to limit the diff to. It will only
211
+ include at least one of the given path or paths.
212
+
213
+ :param create_patch:
214
+ If ``True``, the returned :class:`Diff` contains a detailed patch that if
215
+ applied makes the self to other. Patches are somewhat costly as blobs have
216
+ to be read and diffed.
217
+
218
+ :param kwargs:
219
+ Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
220
+ swap both sides of the diff.
221
+
222
+ :return:
223
+ A :class:`DiffIndex` representing the computed diff.
224
+
225
+ :note:
226
+ On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
227
+ an instance of :class:`~git.objects.tree.Tree` or
228
+ :class:`~git.objects.commit.Commit`, or a git command error will occur.
229
+ """
230
+ args: List[Union[PathLike, Diffable]] = []
231
+ args.append("--abbrev=40") # We need full shas.
232
+ args.append("--full-index") # Get full index paths, not only filenames.
233
+
234
+ # Remove default '-M' arg (check for renames) if user is overriding it.
235
+ if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
236
+ args.append("-M")
237
+
238
+ if create_patch:
239
+ args.append("-p")
240
+ args.append("--no-ext-diff")
241
+ else:
242
+ args.append("--raw")
243
+ args.append("-z")
244
+
245
+ # Ensure we never see colored output.
246
+ # Fixes: https://github.com/gitpython-developers/GitPython/issues/172
247
+ args.append("--no-color")
248
+
249
+ if paths is not None and not isinstance(paths, (tuple, list)):
250
+ paths = [paths]
251
+
252
+ diff_cmd = self.repo.git.diff
253
+ if other is INDEX:
254
+ args.insert(0, "--cached")
255
+ elif other is NULL_TREE:
256
+ args.insert(0, "-r") # Recursive diff-tree.
257
+ args.insert(0, "--root")
258
+ diff_cmd = self.repo.git.diff_tree
259
+ elif other is not None:
260
+ args.insert(0, "-r") # Recursive diff-tree.
261
+ args.insert(0, other)
262
+ diff_cmd = self.repo.git.diff_tree
263
+
264
+ args.insert(0, self)
265
+
266
+ # paths is a list or tuple here, or None.
267
+ if paths:
268
+ args.append("--")
269
+ args.extend(paths)
270
+ # END paths handling
271
+
272
+ kwargs["as_process"] = True
273
+ proc = diff_cmd(*self._process_diff_args(args), **kwargs)
274
+
275
+ diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format
276
+ index = diff_method(self.repo, proc)
277
+
278
+ proc.wait()
279
+ return index
280
+
281
+
282
+ T_Diff = TypeVar("T_Diff", bound="Diff")
283
+
284
+
285
+ class DiffIndex(List[T_Diff]):
286
+ R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
287
+ properties.
288
+
289
+ The class improves the diff handling convenience.
290
+ """
291
+
292
+ change_type = ("A", "C", "D", "R", "M", "T")
293
+ """Change type invariant identifying possible ways a blob can have changed:
294
+
295
+ * ``A`` = Added
296
+ * ``D`` = Deleted
297
+ * ``R`` = Renamed
298
+ * ``M`` = Modified
299
+ * ``T`` = Changed in the type
300
+ """
301
+
302
+ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
303
+ """
304
+ :return:
305
+ Iterator yielding :class:`Diff` instances that match the given `change_type`
306
+
307
+ :param change_type:
308
+ Member of :attr:`DiffIndex.change_type`, namely:
309
+
310
+ * 'A' for added paths
311
+ * 'D' for deleted paths
312
+ * 'R' for renamed paths
313
+ * 'M' for paths with modified data
314
+ * 'T' for changed in the type paths
315
+ """
316
+ if change_type not in self.change_type:
317
+ raise ValueError("Invalid change type: %s" % change_type)
318
+
319
+ for diffidx in self:
320
+ if diffidx.change_type == change_type:
321
+ yield diffidx
322
+ elif change_type == "A" and diffidx.new_file:
323
+ yield diffidx
324
+ elif change_type == "D" and diffidx.deleted_file:
325
+ yield diffidx
326
+ elif change_type == "C" and diffidx.copied_file:
327
+ yield diffidx
328
+ elif change_type == "R" and diffidx.renamed_file:
329
+ yield diffidx
330
+ elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
331
+ yield diffidx
332
+ # END for each diff
333
+
334
+
335
+ class Diff:
336
+ """A Diff contains diff information between two Trees.
337
+
338
+ It contains two sides a and b of the diff. Members are prefixed with "a" and "b"
339
+ respectively to indicate that.
340
+
341
+ Diffs keep information about the changed blob objects, the file mode, renames,
342
+ deletions and new files.
343
+
344
+ There are a few cases where ``None`` has to be expected as member variable value:
345
+
346
+ New File::
347
+
348
+ a_mode is None
349
+ a_blob is None
350
+ a_path is None
351
+
352
+ Deleted File::
353
+
354
+ b_mode is None
355
+ b_blob is None
356
+ b_path is None
357
+
358
+ Working Tree Blobs:
359
+
360
+ When comparing to working trees, the working tree blob will have a null hexsha
361
+ as a corresponding object does not yet exist. The mode will be null as well. The
362
+ path will be available, though.
363
+
364
+ If it is listed in a diff, the working tree version of the file must differ from
365
+ the version in the index or tree, and hence has been modified.
366
+ """
367
+
368
+ # Precompiled regex.
369
+ re_header = re.compile(
370
+ rb"""
371
+ ^diff[ ]--git
372
+ [ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n
373
+ (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
374
+ ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
375
+ (?:^similarity[ ]index[ ]\d+%\n
376
+ ^rename[ ]from[ ](?P<rename_from>.*)\n
377
+ ^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))?
378
+ (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
379
+ (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
380
+ (?:^similarity[ ]index[ ]\d+%\n
381
+ ^copy[ ]from[ ].*\n
382
+ ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))?
383
+ (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
384
+ \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
385
+ (?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
386
+ (?:^\+\+\+[ ](?P<b_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
387
+ """,
388
+ re.VERBOSE | re.MULTILINE,
389
+ )
390
+
391
+ # These can be used for comparisons.
392
+ NULL_HEX_SHA = "0" * 40
393
+ NULL_BIN_SHA = b"\0" * 20
394
+
395
+ __slots__ = (
396
+ "a_blob",
397
+ "b_blob",
398
+ "a_mode",
399
+ "b_mode",
400
+ "a_rawpath",
401
+ "b_rawpath",
402
+ "new_file",
403
+ "deleted_file",
404
+ "copied_file",
405
+ "raw_rename_from",
406
+ "raw_rename_to",
407
+ "diff",
408
+ "change_type",
409
+ "score",
410
+ )
411
+
412
+ def __init__(
413
+ self,
414
+ repo: "Repo",
415
+ a_rawpath: Optional[bytes],
416
+ b_rawpath: Optional[bytes],
417
+ a_blob_id: Union[str, bytes, None],
418
+ b_blob_id: Union[str, bytes, None],
419
+ a_mode: Union[bytes, str, None],
420
+ b_mode: Union[bytes, str, None],
421
+ new_file: bool,
422
+ deleted_file: bool,
423
+ copied_file: bool,
424
+ raw_rename_from: Optional[bytes],
425
+ raw_rename_to: Optional[bytes],
426
+ diff: Union[str, bytes, None],
427
+ change_type: Optional[Lit_change_type],
428
+ score: Optional[int],
429
+ ) -> None:
430
+ assert a_rawpath is None or isinstance(a_rawpath, bytes)
431
+ assert b_rawpath is None or isinstance(b_rawpath, bytes)
432
+ self.a_rawpath = a_rawpath
433
+ self.b_rawpath = b_rawpath
434
+
435
+ self.a_mode = mode_str_to_int(a_mode) if a_mode else None
436
+ self.b_mode = mode_str_to_int(b_mode) if b_mode else None
437
+
438
+ # Determine whether this diff references a submodule. If it does then
439
+ # we need to overwrite "repo" to the corresponding submodule's repo instead.
440
+ if repo and a_rawpath:
441
+ for submodule in repo.submodules:
442
+ if submodule.path == a_rawpath.decode(defenc, "replace"):
443
+ if submodule.module_exists():
444
+ repo = submodule.module()
445
+ break
446
+
447
+ self.a_blob: Union["IndexObject", None]
448
+ if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:
449
+ self.a_blob = None
450
+ else:
451
+ self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path)
452
+
453
+ self.b_blob: Union["IndexObject", None]
454
+ if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA:
455
+ self.b_blob = None
456
+ else:
457
+ self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path)
458
+
459
+ self.new_file: bool = new_file
460
+ self.deleted_file: bool = deleted_file
461
+ self.copied_file: bool = copied_file
462
+
463
+ # Be clear and use None instead of empty strings.
464
+ assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
465
+ assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
466
+ self.raw_rename_from = raw_rename_from or None
467
+ self.raw_rename_to = raw_rename_to or None
468
+
469
+ self.diff = diff
470
+ self.change_type: Union[Lit_change_type, None] = change_type
471
+ self.score = score
472
+
473
+ def __eq__(self, other: object) -> bool:
474
+ for name in self.__slots__:
475
+ if getattr(self, name) != getattr(other, name):
476
+ return False
477
+ # END for each name
478
+ return True
479
+
480
+ def __ne__(self, other: object) -> bool:
481
+ return not (self == other)
482
+
483
+ def __hash__(self) -> int:
484
+ return hash(tuple(getattr(self, n) for n in self.__slots__))
485
+
486
+ def __str__(self) -> str:
487
+ h = "%s"
488
+ if self.a_blob:
489
+ h %= self.a_blob.path
490
+ elif self.b_blob:
491
+ h %= self.b_blob.path
492
+
493
+ msg = ""
494
+ line = None
495
+ line_length = 0
496
+ for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")):
497
+ if b:
498
+ line = "\n%s: %o | %s" % (n, b.mode, b.hexsha)
499
+ else:
500
+ line = "\n%s: None" % n
501
+ # END if blob is not None
502
+ line_length = max(len(line), line_length)
503
+ msg += line
504
+ # END for each blob
505
+
506
+ # Add headline.
507
+ h += "\n" + "=" * line_length
508
+
509
+ if self.deleted_file:
510
+ msg += "\nfile deleted in rhs"
511
+ if self.new_file:
512
+ msg += "\nfile added in rhs"
513
+ if self.copied_file:
514
+ msg += "\nfile %r copied from %r" % (self.b_path, self.a_path)
515
+ if self.rename_from:
516
+ msg += "\nfile renamed from %r" % self.rename_from
517
+ if self.rename_to:
518
+ msg += "\nfile renamed to %r" % self.rename_to
519
+ if self.diff:
520
+ msg += "\n---"
521
+ try:
522
+ msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
523
+ except UnicodeDecodeError:
524
+ msg += "OMITTED BINARY DATA"
525
+ # END handle encoding
526
+ msg += "\n---"
527
+ # END diff info
528
+
529
+ return h + msg
530
+
531
+ @property
532
+ def a_path(self) -> Optional[str]:
533
+ return self.a_rawpath.decode(defenc, "replace") if self.a_rawpath else None
534
+
535
+ @property
536
+ def b_path(self) -> Optional[str]:
537
+ return self.b_rawpath.decode(defenc, "replace") if self.b_rawpath else None
538
+
539
+ @property
540
+ def rename_from(self) -> Optional[str]:
541
+ return self.raw_rename_from.decode(defenc, "replace") if self.raw_rename_from else None
542
+
543
+ @property
544
+ def rename_to(self) -> Optional[str]:
545
+ return self.raw_rename_to.decode(defenc, "replace") if self.raw_rename_to else None
546
+
547
+ @property
548
+ def renamed(self) -> bool:
549
+ """Deprecated, use :attr:`renamed_file` instead.
550
+
551
+ :return:
552
+ ``True`` if the blob of our diff has been renamed
553
+
554
+ :note:
555
+ This property is deprecated.
556
+ Please use the :attr:`renamed_file` property instead.
557
+ """
558
+ warnings.warn(
559
+ "Diff.renamed is deprecated, use Diff.renamed_file instead",
560
+ DeprecationWarning,
561
+ stacklevel=2,
562
+ )
563
+ return self.renamed_file
564
+
565
+ @property
566
+ def renamed_file(self) -> bool:
567
+ """:return: ``True`` if the blob of our diff has been renamed"""
568
+ return self.rename_from != self.rename_to
569
+
570
+ @classmethod
571
+ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
572
+ if path_match:
573
+ return decode_path(path_match)
574
+
575
+ if rename_match:
576
+ return decode_path(rename_match, has_ab_prefix=False)
577
+
578
+ if path_fallback_match:
579
+ return decode_path(path_fallback_match)
580
+
581
+ return None
582
+
583
+ @classmethod
584
+ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
585
+ """Create a new :class:`DiffIndex` from the given process output which must be
586
+ in patch format.
587
+
588
+ :param repo:
589
+ The repository we are operating on.
590
+
591
+ :param proc:
592
+ :manpage:`git-diff(1)` process to read from
593
+ (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
594
+
595
+ :return:
596
+ :class:`DiffIndex`
597
+ """
598
+
599
+ # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
600
+ text_list: List[bytes] = []
601
+ handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)
602
+
603
+ # For now, we have to bake the stream.
604
+ text = b"".join(text_list)
605
+ index: "DiffIndex" = DiffIndex()
606
+ previous_header: Union[Match[bytes], None] = None
607
+ header: Union[Match[bytes], None] = None
608
+ a_path, b_path = None, None # For mypy.
609
+ a_mode, b_mode = None, None # For mypy.
610
+ for _header in cls.re_header.finditer(text):
611
+ (
612
+ a_path_fallback,
613
+ b_path_fallback,
614
+ old_mode,
615
+ new_mode,
616
+ rename_from,
617
+ rename_to,
618
+ new_file_mode,
619
+ deleted_file_mode,
620
+ copied_file_name,
621
+ a_blob_id,
622
+ b_blob_id,
623
+ b_mode,
624
+ a_path,
625
+ b_path,
626
+ ) = _header.groups()
627
+
628
+ new_file, deleted_file, copied_file = (
629
+ bool(new_file_mode),
630
+ bool(deleted_file_mode),
631
+ bool(copied_file_name),
632
+ )
633
+
634
+ a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
635
+ b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
636
+
637
+ # Our only means to find the actual text is to see what has not been matched
638
+ # by our regex, and then retro-actively assign it to our index.
639
+ if previous_header is not None:
640
+ index[-1].diff = text[previous_header.end() : _header.start()]
641
+ # END assign actual diff
642
+
643
+ # Make sure the mode is set if the path is set. Otherwise the resulting blob
644
+ # is invalid. We just use the one mode we should have parsed.
645
+ a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
646
+ b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
647
+ index.append(
648
+ Diff(
649
+ repo,
650
+ a_path,
651
+ b_path,
652
+ a_blob_id and a_blob_id.decode(defenc),
653
+ b_blob_id and b_blob_id.decode(defenc),
654
+ a_mode and a_mode.decode(defenc),
655
+ b_mode and b_mode.decode(defenc),
656
+ new_file,
657
+ deleted_file,
658
+ copied_file,
659
+ rename_from,
660
+ rename_to,
661
+ None,
662
+ None,
663
+ None,
664
+ )
665
+ )
666
+
667
+ previous_header = _header
668
+ header = _header
669
+ # END for each header we parse
670
+ if index and header:
671
+ index[-1].diff = text[header.end() :]
672
+ # END assign last diff
673
+
674
+ return index
675
+
676
+ @staticmethod
677
+ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
678
+ lines = lines_bytes.decode(defenc)
679
+
680
+ # Discard everything before the first colon, and the colon itself.
681
+ _, _, lines = lines.partition(":")
682
+
683
+ for line in lines.split("\x00:"):
684
+ if not line:
685
+ # The line data is empty, skip.
686
+ continue
687
+ meta, _, path = line.partition("\x00")
688
+ path = path.rstrip("\x00")
689
+ a_blob_id: Optional[str]
690
+ b_blob_id: Optional[str]
691
+ old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
692
+ # Change type can be R100
693
+ # R: status letter
694
+ # 100: score (in case of copy and rename)
695
+ change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
696
+ score_str = "".join(_change_type[1:])
697
+ score = int(score_str) if score_str.isdigit() else None
698
+ path = path.strip("\n")
699
+ a_path = path.encode(defenc)
700
+ b_path = path.encode(defenc)
701
+ deleted_file = False
702
+ new_file = False
703
+ copied_file = False
704
+ rename_from = None
705
+ rename_to = None
706
+
707
+ # NOTE: We cannot conclude from the existence of a blob to change type,
708
+ # as diffs with the working do not have blobs yet.
709
+ if change_type == "D":
710
+ b_blob_id = None # Optional[str]
711
+ deleted_file = True
712
+ elif change_type == "A":
713
+ a_blob_id = None
714
+ new_file = True
715
+ elif change_type == "C":
716
+ copied_file = True
717
+ a_path_str, b_path_str = path.split("\x00", 1)
718
+ a_path = a_path_str.encode(defenc)
719
+ b_path = b_path_str.encode(defenc)
720
+ elif change_type == "R":
721
+ a_path_str, b_path_str = path.split("\x00", 1)
722
+ a_path = a_path_str.encode(defenc)
723
+ b_path = b_path_str.encode(defenc)
724
+ rename_from, rename_to = a_path, b_path
725
+ elif change_type == "T":
726
+ # Nothing to do.
727
+ pass
728
+ # END add/remove handling
729
+
730
+ diff = Diff(
731
+ repo,
732
+ a_path,
733
+ b_path,
734
+ a_blob_id,
735
+ b_blob_id,
736
+ old_mode,
737
+ new_mode,
738
+ new_file,
739
+ deleted_file,
740
+ copied_file,
741
+ rename_from,
742
+ rename_to,
743
+ "",
744
+ change_type,
745
+ score,
746
+ )
747
+ index.append(diff)
748
+
749
+ @classmethod
750
+ def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
751
+ """Create a new :class:`DiffIndex` from the given process output which must be
752
+ in raw format.
753
+
754
+ :param repo:
755
+ The repository we are operating on.
756
+
757
+ :param proc:
758
+ Process to read output from.
759
+
760
+ :return:
761
+ :class:`DiffIndex`
762
+ """
763
+ # handles
764
+ # :100644 100644 687099101... 37c5e30c8... M .gitignore
765
+
766
+ index: "DiffIndex" = DiffIndex()
767
+ handle_process_output(
768
+ proc,
769
+ lambda byt: cls._handle_diff_line(byt, repo, index),
770
+ None,
771
+ finalize_process,
772
+ decode_streams=False,
773
+ )
774
+
775
+ return index
.venv/lib/python3.11/site-packages/git/exc.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Exceptions thrown throughout the git package."""
7
+
8
+ __all__ = [
9
+ # Defined in gitdb.exc:
10
+ "AmbiguousObjectName",
11
+ "BadName",
12
+ "BadObject",
13
+ "BadObjectType",
14
+ "InvalidDBRoot",
15
+ "ODBError",
16
+ "ParseError",
17
+ "UnsupportedOperation",
18
+ # Introduced in this module:
19
+ "GitError",
20
+ "InvalidGitRepositoryError",
21
+ "WorkTreeRepositoryUnsupported",
22
+ "NoSuchPathError",
23
+ "UnsafeProtocolError",
24
+ "UnsafeOptionError",
25
+ "CommandError",
26
+ "GitCommandNotFound",
27
+ "GitCommandError",
28
+ "CheckoutError",
29
+ "CacheError",
30
+ "UnmergedEntriesError",
31
+ "HookExecutionError",
32
+ "RepositoryDirtyError",
33
+ ]
34
+
35
+ from gitdb.exc import (
36
+ AmbiguousObjectName,
37
+ BadName,
38
+ BadObject,
39
+ BadObjectType,
40
+ InvalidDBRoot,
41
+ ODBError,
42
+ ParseError,
43
+ UnsupportedOperation,
44
+ )
45
+
46
+ from git.compat import safe_decode
47
+ from git.util import remove_password_if_present
48
+
49
+ # typing ----------------------------------------------------
50
+
51
+ from typing import List, Sequence, Tuple, TYPE_CHECKING, Union
52
+
53
+ from git.types import PathLike
54
+
55
+ if TYPE_CHECKING:
56
+ from git.repo.base import Repo
57
+
58
+ # ------------------------------------------------------------------
59
+
60
+
61
+ class GitError(Exception):
62
+ """Base class for all package exceptions."""
63
+
64
+
65
+ class InvalidGitRepositoryError(GitError):
66
+ """Thrown if the given repository appears to have an invalid format."""
67
+
68
+
69
+ class WorkTreeRepositoryUnsupported(InvalidGitRepositoryError):
70
+ """Thrown to indicate we can't handle work tree repositories."""
71
+
72
+
73
+ class NoSuchPathError(GitError, OSError):
74
+ """Thrown if a path could not be access by the system."""
75
+
76
+
77
+ class UnsafeProtocolError(GitError):
78
+ """Thrown if unsafe protocols are passed without being explicitly allowed."""
79
+
80
+
81
+ class UnsafeOptionError(GitError):
82
+ """Thrown if unsafe options are passed without being explicitly allowed."""
83
+
84
+
85
+ class CommandError(GitError):
86
+ """Base class for exceptions thrown at every stage of :class:`~subprocess.Popen`
87
+ execution.
88
+
89
+ :param command:
90
+ A non-empty list of argv comprising the command-line.
91
+ """
92
+
93
+ _msg = "Cmd('%s') failed%s"
94
+ """Format string with 2 ``%s`` for ``<cmdline>`` and the rest.
95
+
96
+ For example: ``"'%s' failed%s"``
97
+
98
+ Subclasses may override this attribute, provided it is still in this form.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ command: Union[List[str], Tuple[str, ...], str],
104
+ status: Union[str, int, None, Exception] = None,
105
+ stderr: Union[bytes, str, None] = None,
106
+ stdout: Union[bytes, str, None] = None,
107
+ ) -> None:
108
+ if not isinstance(command, (tuple, list)):
109
+ command = command.split()
110
+ self.command = remove_password_if_present(command)
111
+ self.status = status
112
+ if status:
113
+ if isinstance(status, Exception):
114
+ status = "%s('%s')" % (type(status).__name__, safe_decode(str(status)))
115
+ else:
116
+ try:
117
+ status = "exit code(%s)" % int(status)
118
+ except (ValueError, TypeError):
119
+ s = safe_decode(str(status))
120
+ status = "'%s'" % s if isinstance(status, str) else s
121
+
122
+ self._cmd = safe_decode(self.command[0])
123
+ self._cmdline = " ".join(safe_decode(i) for i in self.command)
124
+ self._cause = status and " due to: %s" % status or "!"
125
+ stdout_decode = safe_decode(stdout)
126
+ stderr_decode = safe_decode(stderr)
127
+ self.stdout = stdout_decode and "\n stdout: '%s'" % stdout_decode or ""
128
+ self.stderr = stderr_decode and "\n stderr: '%s'" % stderr_decode or ""
129
+
130
+ def __str__(self) -> str:
131
+ return (self._msg + "\n cmdline: %s%s%s") % (
132
+ self._cmd,
133
+ self._cause,
134
+ self._cmdline,
135
+ self.stdout,
136
+ self.stderr,
137
+ )
138
+
139
+
140
+ class GitCommandNotFound(CommandError):
141
+ """Thrown if we cannot find the ``git`` executable in the :envvar:`PATH` or at the
142
+ path given by the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable."""
143
+
144
+ def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
145
+ super().__init__(command, cause)
146
+ self._msg = "Cmd('%s') not found%s"
147
+
148
+
149
+ class GitCommandError(CommandError):
150
+ """Thrown if execution of the git command fails with non-zero status code."""
151
+
152
+ def __init__(
153
+ self,
154
+ command: Union[List[str], Tuple[str, ...], str],
155
+ status: Union[str, int, None, Exception] = None,
156
+ stderr: Union[bytes, str, None] = None,
157
+ stdout: Union[bytes, str, None] = None,
158
+ ) -> None:
159
+ super().__init__(command, status, stderr, stdout)
160
+
161
+
162
+ class CheckoutError(GitError):
163
+ """Thrown if a file could not be checked out from the index as it contained
164
+ changes.
165
+
166
+ The :attr:`failed_files` attribute contains a list of relative paths that failed to
167
+ be checked out as they contained changes that did not exist in the index.
168
+
169
+ The :attr:`failed_reasons` attribute contains a string informing about the actual
170
+ cause of the issue.
171
+
172
+ The :attr:`valid_files` attribute contains a list of relative paths to files that
173
+ were checked out successfully and hence match the version stored in the index.
174
+ """
175
+
176
+ def __init__(
177
+ self,
178
+ message: str,
179
+ failed_files: Sequence[PathLike],
180
+ valid_files: Sequence[PathLike],
181
+ failed_reasons: List[str],
182
+ ) -> None:
183
+ Exception.__init__(self, message)
184
+ self.failed_files = failed_files
185
+ self.failed_reasons = failed_reasons
186
+ self.valid_files = valid_files
187
+
188
+ def __str__(self) -> str:
189
+ return Exception.__str__(self) + ":%s" % self.failed_files
190
+
191
+
192
+ class CacheError(GitError):
193
+ """Base for all errors related to the git index, which is called "cache"
194
+ internally."""
195
+
196
+
197
+ class UnmergedEntriesError(CacheError):
198
+ """Thrown if an operation cannot proceed as there are still unmerged
199
+ entries in the cache."""
200
+
201
+
202
+ class HookExecutionError(CommandError):
203
+ """Thrown if a hook exits with a non-zero exit code.
204
+
205
+ This provides access to the exit code and the string returned via standard output.
206
+ """
207
+
208
+ def __init__(
209
+ self,
210
+ command: Union[List[str], Tuple[str, ...], str],
211
+ status: Union[str, int, None, Exception],
212
+ stderr: Union[bytes, str, None] = None,
213
+ stdout: Union[bytes, str, None] = None,
214
+ ) -> None:
215
+ super().__init__(command, status, stderr, stdout)
216
+ self._msg = "Hook('%s') failed%s"
217
+
218
+
219
+ class RepositoryDirtyError(GitError):
220
+ """Thrown whenever an operation on a repository fails as it has uncommitted changes
221
+ that would be overwritten."""
222
+
223
+ def __init__(self, repo: "Repo", message: str) -> None:
224
+ self.repo = repo
225
+ self.message = message
226
+
227
+ def __str__(self) -> str:
228
+ return "Operation cannot be performed on %r: %s" % (self.repo, self.message)
.venv/lib/python3.11/site-packages/git/index/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Initialize the index package."""
5
+
6
+ __all__ = [
7
+ "BaseIndexEntry",
8
+ "BlobFilter",
9
+ "CheckoutError",
10
+ "IndexEntry",
11
+ "IndexFile",
12
+ "StageType",
13
+ ]
14
+
15
+ from .base import CheckoutError, IndexFile
16
+ from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType
.venv/lib/python3.11/site-packages/git/index/base.py ADDED
@@ -0,0 +1,1532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
7
+ of index manipulations such as querying and merging."""
8
+
9
+ __all__ = ["IndexFile", "CheckoutError", "StageType"]
10
+
11
+ import contextlib
12
+ import datetime
13
+ import glob
14
+ from io import BytesIO
15
+ import os
16
+ import os.path as osp
17
+ from stat import S_ISLNK
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+
22
+ from gitdb.base import IStream
23
+ from gitdb.db import MemoryDB
24
+
25
+ from git.compat import defenc, force_bytes
26
+ import git.diff as git_diff
27
+ from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
28
+ from git.objects import Blob, Commit, Object, Submodule, Tree
29
+ from git.objects.util import Serializable
30
+ from git.util import (
31
+ Actor,
32
+ LazyMixin,
33
+ LockedFD,
34
+ join_path_native,
35
+ file_contents_ro,
36
+ to_native_path_linux,
37
+ unbare_repo,
38
+ to_bin_sha,
39
+ )
40
+
41
+ from .fun import (
42
+ S_IFGITLINK,
43
+ aggressive_tree_merge,
44
+ entry_key,
45
+ read_cache,
46
+ run_commit_hook,
47
+ stat_mode_to_index_mode,
48
+ write_cache,
49
+ write_tree_from_cache,
50
+ )
51
+ from .typ import BaseIndexEntry, IndexEntry, StageType
52
+ from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
53
+
54
+ # typing -----------------------------------------------------------------------------
55
+
56
+ from typing import (
57
+ Any,
58
+ BinaryIO,
59
+ Callable,
60
+ Dict,
61
+ Generator,
62
+ IO,
63
+ Iterable,
64
+ Iterator,
65
+ List,
66
+ NoReturn,
67
+ Sequence,
68
+ TYPE_CHECKING,
69
+ Tuple,
70
+ Union,
71
+ )
72
+
73
+ from git.types import Literal, PathLike
74
+
75
+ if TYPE_CHECKING:
76
+ from subprocess import Popen
77
+
78
+ from git.refs.reference import Reference
79
+ from git.repo import Repo
80
+
81
+
82
+ Treeish = Union[Tree, Commit, str, bytes]
83
+
84
+ # ------------------------------------------------------------------------------------
85
+
86
+
87
+ @contextlib.contextmanager
88
+ def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
89
+ """Create a named temporary file git subprocesses can open, deleting it afterward.
90
+
91
+ :param directory:
92
+ The directory in which the file is created.
93
+
94
+ :return:
95
+ A context manager object that creates the file and provides its name on entry,
96
+ and deletes it on exit.
97
+ """
98
+ if sys.platform == "win32":
99
+ fd, name = tempfile.mkstemp(dir=directory)
100
+ os.close(fd)
101
+ try:
102
+ yield name
103
+ finally:
104
+ os.remove(name)
105
+ else:
106
+ with tempfile.NamedTemporaryFile(dir=directory) as ctx:
107
+ yield ctx.name
108
+
109
+
110
+ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
111
+ """An Index that can be manipulated using a native implementation in order to save
112
+ git command function calls wherever possible.
113
+
114
+ This provides custom merging facilities allowing to merge without actually changing
115
+ your index or your working tree. This way you can perform your own test merges based
116
+ on the index only without having to deal with the working copy. This is useful in
117
+ case of partial working trees.
118
+
119
+ Entries:
120
+
121
+ The index contains an entries dict whose keys are tuples of type
122
+ :class:`~git.index.typ.IndexEntry` to facilitate access.
123
+
124
+ You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
125
+
126
+ index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
127
+
128
+ Make sure you use :meth:`index.write() <write>` once you are done manipulating the
129
+ index directly before operating on it using the git command.
130
+ """
131
+
132
+ __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
133
+
134
+ _VERSION = 2
135
+ """The latest version we support."""
136
+
137
+ S_IFGITLINK = S_IFGITLINK
138
+ """Flags for a submodule."""
139
+
140
+ def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
141
+ """Initialize this Index instance, optionally from the given `file_path`.
142
+
143
+ If no `file_path` is given, we will be created from the current index file.
144
+
145
+ If a stream is not given, the stream will be initialized from the current
146
+ repository's index on demand.
147
+ """
148
+ self.repo = repo
149
+ self.version = self._VERSION
150
+ self._extension_data = b""
151
+ self._file_path: PathLike = file_path or self._index_path()
152
+
153
+ def _set_cache_(self, attr: str) -> None:
154
+ if attr == "entries":
155
+ try:
156
+ fd = os.open(self._file_path, os.O_RDONLY)
157
+ except OSError:
158
+ # In new repositories, there may be no index, which means we are empty.
159
+ self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
160
+ return
161
+ # END exception handling
162
+
163
+ try:
164
+ stream = file_contents_ro(fd, stream=True, allow_mmap=True)
165
+ finally:
166
+ os.close(fd)
167
+
168
+ self._deserialize(stream)
169
+ else:
170
+ super()._set_cache_(attr)
171
+
172
+ def _index_path(self) -> PathLike:
173
+ if self.repo.git_dir:
174
+ return join_path_native(self.repo.git_dir, "index")
175
+ else:
176
+ raise GitCommandError("No git directory given to join index path")
177
+
178
+ @property
179
+ def path(self) -> PathLike:
180
+ """:return: Path to the index file we are representing"""
181
+ return self._file_path
182
+
183
+ def _delete_entries_cache(self) -> None:
184
+ """Safely clear the entries cache so it can be recreated."""
185
+ try:
186
+ del self.entries
187
+ except AttributeError:
188
+ # It failed in Python 2.6.5 with AttributeError.
189
+ # FIXME: Look into whether we can just remove this except clause now.
190
+ pass
191
+ # END exception handling
192
+
193
+ # { Serializable Interface
194
+
195
+ def _deserialize(self, stream: IO) -> "IndexFile":
196
+ """Initialize this instance with index values read from the given stream."""
197
+ self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
198
+ return self
199
+
200
+ def _entries_sorted(self) -> List[IndexEntry]:
201
+ """:return: List of entries, in a sorted fashion, first by path, then by stage"""
202
+ return sorted(self.entries.values(), key=lambda e: (e.path, e.stage))
203
+
204
+ def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile":
205
+ entries = self._entries_sorted()
206
+ extension_data = self._extension_data # type: Union[None, bytes]
207
+ if ignore_extension_data:
208
+ extension_data = None
209
+ write_cache(entries, stream, extension_data)
210
+ return self
211
+
212
+ # } END serializable interface
213
+
214
+ def write(
215
+ self,
216
+ file_path: Union[None, PathLike] = None,
217
+ ignore_extension_data: bool = False,
218
+ ) -> None:
219
+ """Write the current state to our file path or to the given one.
220
+
221
+ :param file_path:
222
+ If ``None``, we will write to our stored file path from which we have been
223
+ initialized. Otherwise we write to the given file path. Please note that
224
+ this will change the `file_path` of this index to the one you gave.
225
+
226
+ :param ignore_extension_data:
227
+ If ``True``, the TREE type extension data read in the index will not be
228
+ written to disk. NOTE that no extension data is actually written. Use this
229
+ if you have altered the index and would like to use
230
+ :manpage:`git-write-tree(1)` afterwards to create a tree representing your
231
+ written changes. If this data is present in the written index,
232
+ :manpage:`git-write-tree(1)` will instead write the stored/cached tree.
233
+ Alternatively, use :meth:`write_tree` to handle this case automatically.
234
+ """
235
+ # Make sure we have our entries read before getting a write lock.
236
+ # Otherwise it would be done when streaming.
237
+ # This can happen if one doesn't change the index, but writes it right away.
238
+ self.entries # noqa: B018
239
+ lfd = LockedFD(file_path or self._file_path)
240
+ stream = lfd.open(write=True, stream=True)
241
+
242
+ try:
243
+ self._serialize(stream, ignore_extension_data)
244
+ except BaseException:
245
+ lfd.rollback()
246
+ raise
247
+
248
+ lfd.commit()
249
+
250
+ # Make sure we represent what we have written.
251
+ if file_path is not None:
252
+ self._file_path = file_path
253
+
254
+ @post_clear_cache
255
+ @default_index
256
+ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
257
+ """Merge the given `rhs` treeish into the current index, possibly taking
258
+ a common base treeish into account.
259
+
260
+ As opposed to the :func:`from_tree` method, this allows you to use an already
261
+ existing tree as the left side of the merge.
262
+
263
+ :param rhs:
264
+ Treeish reference pointing to the 'other' side of the merge.
265
+
266
+ :param base:
267
+ Optional treeish reference pointing to the common base of `rhs` and this
268
+ index which equals lhs.
269
+
270
+ :return:
271
+ self (containing the merge and possibly unmerged entries in case of
272
+ conflicts)
273
+
274
+ :raise git.exc.GitCommandError:
275
+ If there is a merge conflict. The error will be raised at the first
276
+ conflicting path. If you want to have proper merge resolution to be done by
277
+ yourself, you have to commit the changed index (or make a valid tree from
278
+ it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
279
+ """
280
+ # -i : ignore working tree status
281
+ # --aggressive : handle more merge cases
282
+ # -m : do an actual merge
283
+ args: List[Union[Treeish, str]] = ["--aggressive", "-i", "-m"]
284
+ if base is not None:
285
+ args.append(base)
286
+ args.append(rhs)
287
+
288
+ self.repo.git.read_tree(args)
289
+ return self
290
+
291
+ @classmethod
292
+ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
293
+ """Merge the given treeish revisions into a new index which is returned.
294
+
295
+ This method behaves like ``git-read-tree --aggressive`` when doing the merge.
296
+
297
+ :param repo:
298
+ The repository treeish are located in.
299
+
300
+ :param tree_sha:
301
+ 20 byte or 40 byte tree sha or tree objects.
302
+
303
+ :return:
304
+ New :class:`IndexFile` instance. Its path will be undefined.
305
+ If you intend to write such a merged Index, supply an alternate
306
+ ``file_path`` to its :meth:`write` method.
307
+ """
308
+ tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
309
+ base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
310
+
311
+ inst = cls(repo)
312
+ # Convert to entries dict.
313
+ entries: Dict[Tuple[PathLike, int], IndexEntry] = dict(
314
+ zip(
315
+ ((e.path, e.stage) for e in base_entries),
316
+ (IndexEntry.from_base(e) for e in base_entries),
317
+ )
318
+ )
319
+
320
+ inst.entries = entries
321
+ return inst
322
+
323
+ @classmethod
324
+ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
325
+ R"""Merge the given treeish revisions into a new index which is returned.
326
+ The original index will remain unaltered.
327
+
328
+ :param repo:
329
+ The repository treeish are located in.
330
+
331
+ :param treeish:
332
+ One, two or three :class:`~git.objects.tree.Tree` objects,
333
+ :class:`~git.objects.commit.Commit`\s or 40 byte hexshas.
334
+
335
+ The result changes according to the amount of trees:
336
+
337
+ 1. If 1 Tree is given, it will just be read into a new index.
338
+ 2. If 2 Trees are given, they will be merged into a new index using a two
339
+ way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
340
+ one. It behaves like a fast-forward.
341
+ 3. If 3 Trees are given, a 3-way merge will be performed with the first tree
342
+ being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current'
343
+ tree, tree 3 is the 'other' one.
344
+
345
+ :param kwargs:
346
+ Additional arguments passed to :manpage:`git-read-tree(1)`.
347
+
348
+ :return:
349
+ New :class:`IndexFile` instance. It will point to a temporary index location
350
+ which does not exist anymore. If you intend to write such a merged Index,
351
+ supply an alternate ``file_path`` to its :meth:`write` method.
352
+
353
+ :note:
354
+ In the three-way merge case, ``--aggressive`` will be specified to
355
+ automatically resolve more cases in a commonly correct manner. Specify
356
+ ``trivial=True`` as a keyword argument to override that.
357
+
358
+ As the underlying :manpage:`git-read-tree(1)` command takes into account the
359
+ current index, it will be temporarily moved out of the way to prevent any
360
+ unexpected interference.
361
+ """
362
+ if len(treeish) == 0 or len(treeish) > 3:
363
+ raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
364
+
365
+ arg_list: List[Union[Treeish, str]] = []
366
+ # Ignore that the working tree and index possibly are out of date.
367
+ if len(treeish) > 1:
368
+ # Drop unmerged entries when reading our index and merging.
369
+ arg_list.append("--reset")
370
+ # Handle non-trivial cases the way a real merge does.
371
+ arg_list.append("--aggressive")
372
+ # END merge handling
373
+
374
+ # Create the temporary file in the .git directory to be sure renaming
375
+ # works - /tmp/ directories could be on another device.
376
+ with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index:
377
+ arg_list.append("--index-output=%s" % tmp_index)
378
+ arg_list.extend(treeish)
379
+
380
+ # Move the current index out of the way - otherwise the merge may fail as it
381
+ # considers existing entries. Moving it essentially clears the index.
382
+ # Unfortunately there is no 'soft' way to do it.
383
+ # The TemporaryFileSwap ensures the original file gets put back.
384
+ with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
385
+ repo.git.read_tree(*arg_list, **kwargs)
386
+ index = cls(repo, tmp_index)
387
+ index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file.
388
+ return index
389
+ # END index merge handling
390
+
391
+ # UTILITIES
392
+
393
+ @unbare_repo
394
+ def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
395
+ """Expand the directories in list of paths to the corresponding paths
396
+ accordingly.
397
+
398
+ :note:
399
+ git will add items multiple times even if a glob overlapped with manually
400
+ specified paths or if paths where specified multiple times - we respect that
401
+ and do not prune.
402
+ """
403
+
404
+ def raise_exc(e: Exception) -> NoReturn:
405
+ raise e
406
+
407
+ r = str(self.repo.working_tree_dir)
408
+ rs = r + os.sep
409
+ for path in paths:
410
+ abs_path = str(path)
411
+ if not osp.isabs(abs_path):
412
+ abs_path = osp.join(r, path)
413
+ # END make absolute path
414
+
415
+ try:
416
+ st = os.lstat(abs_path) # Handles non-symlinks as well.
417
+ except OSError:
418
+ # The lstat call may fail as the path may contain globs as well.
419
+ pass
420
+ else:
421
+ if S_ISLNK(st.st_mode):
422
+ yield abs_path.replace(rs, "")
423
+ continue
424
+ # END check symlink
425
+
426
+ # If the path is not already pointing to an existing file, resolve globs if possible.
427
+ if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
428
+ resolved_paths = glob.glob(abs_path)
429
+ # not abs_path in resolved_paths:
430
+ # A glob() resolving to the same path we are feeding it with is a
431
+ # glob() that failed to resolve. If we continued calling ourselves
432
+ # we'd endlessly recurse. If the condition below evaluates to true
433
+ # then we are likely dealing with a file whose name contains wildcard
434
+ # characters.
435
+ if abs_path not in resolved_paths:
436
+ for f in self._iter_expand_paths(glob.glob(abs_path)):
437
+ yield str(f).replace(rs, "")
438
+ continue
439
+ # END glob handling
440
+ try:
441
+ for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
442
+ for rela_file in files:
443
+ # Add relative paths only.
444
+ yield osp.join(root.replace(rs, ""), rela_file)
445
+ # END for each file in subdir
446
+ # END for each subdirectory
447
+ except OSError:
448
+ # It was a file or something that could not be iterated.
449
+ yield abs_path.replace(rs, "")
450
+ # END path exception handling
451
+ # END for each path
452
+
453
+ def _write_path_to_stdin(
454
+ self,
455
+ proc: "Popen",
456
+ filepath: PathLike,
457
+ item: PathLike,
458
+ fmakeexc: Callable[..., GitError],
459
+ fprogress: Callable[[PathLike, bool, PathLike], None],
460
+ read_from_stdout: bool = True,
461
+ ) -> Union[None, str]:
462
+ """Write path to ``proc.stdin`` and make sure it processes the item, including
463
+ progress.
464
+
465
+ :return:
466
+ stdout string
467
+
468
+ :param read_from_stdout:
469
+ If ``True``, ``proc.stdout`` will be read after the item was sent to stdin.
470
+ In that case, it will return ``None``.
471
+
472
+ :note:
473
+ There is a bug in :manpage:`git-update-index(1)` that prevents it from
474
+ sending reports just in time. This is why we have a version that tries to
475
+ read stdout and one which doesn't. In fact, the stdout is not important as
476
+ the piped-in files are processed anyway and just in time.
477
+
478
+ :note:
479
+ Newlines are essential here, git's behaviour is somewhat inconsistent on
480
+ this depending on the version, hence we try our best to deal with newlines
481
+ carefully. Usually the last newline will not be sent, instead we will close
482
+ stdin to break the pipe.
483
+ """
484
+ fprogress(filepath, False, item)
485
+ rval: Union[None, str] = None
486
+
487
+ if proc.stdin is not None:
488
+ try:
489
+ proc.stdin.write(("%s\n" % filepath).encode(defenc))
490
+ except IOError as e:
491
+ # Pipe broke, usually because some error happened.
492
+ raise fmakeexc() from e
493
+ # END write exception handling
494
+ proc.stdin.flush()
495
+
496
+ if read_from_stdout and proc.stdout is not None:
497
+ rval = proc.stdout.readline().strip()
498
+ fprogress(filepath, True, item)
499
+ return rval
500
+
501
+ def iter_blobs(
502
+ self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True
503
+ ) -> Iterator[Tuple[StageType, Blob]]:
504
+ """
505
+ :return:
506
+ Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and
507
+ stages, tuple(stage, Blob).
508
+
509
+ :param predicate:
510
+ Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
511
+ the iterator. A default filter, the :class:`~git.index.typ.BlobFilter`, allows you
512
+ to yield blobs only if they match a given list of paths.
513
+ """
514
+ for entry in self.entries.values():
515
+ blob = entry.to_blob(self.repo)
516
+ blob.size = entry.size
517
+ output = (entry.stage, blob)
518
+ if predicate(output):
519
+ yield output
520
+ # END for each entry
521
+
522
+ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
523
+ """
524
+ :return:
525
+ Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a
526
+ path in the index with a list containing sorted stage/blob pairs.
527
+
528
+ :note:
529
+ Blobs that have been removed in one side simply do not exist in the given
530
+ stage. That is, a file removed on the 'other' branch whose entries are at
531
+ stage 3 will not have a stage 3 entry.
532
+ """
533
+
534
+ def is_unmerged_blob(t: Tuple[StageType, Blob]) -> bool:
535
+ return t[0] != 0
536
+
537
+ path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
538
+ for stage, blob in self.iter_blobs(is_unmerged_blob):
539
+ path_map.setdefault(blob.path, []).append((stage, blob))
540
+ # END for each unmerged blob
541
+ for line in path_map.values():
542
+ line.sort()
543
+
544
+ return path_map
545
+
546
+ @classmethod
547
+ def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]:
548
+ return entry_key(*entry)
549
+
550
+ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
551
+ """Resolve the blobs given in blob iterator.
552
+
553
+ This will effectively remove the index entries of the respective path at all
554
+ non-null stages and add the given blob as new stage null blob.
555
+
556
+ For each path there may only be one blob, otherwise a :exc:`ValueError` will be
557
+ raised claiming the path is already at stage 0.
558
+
559
+ :raise ValueError:
560
+ If one of the blobs already existed at stage 0.
561
+
562
+ :return:
563
+ self
564
+
565
+ :note:
566
+ You will have to write the index manually once you are done, i.e.
567
+ ``index.resolve_blobs(blobs).write()``.
568
+ """
569
+ for blob in iter_blobs:
570
+ stage_null_key = (blob.path, 0)
571
+ if stage_null_key in self.entries:
572
+ raise ValueError("Path %r already exists at stage 0" % str(blob.path))
573
+ # END assert blob is not stage 0 already
574
+
575
+ # Delete all possible stages.
576
+ for stage in (1, 2, 3):
577
+ try:
578
+ del self.entries[(blob.path, stage)]
579
+ except KeyError:
580
+ pass
581
+ # END ignore key errors
582
+ # END for each possible stage
583
+
584
+ self.entries[stage_null_key] = IndexEntry.from_blob(blob)
585
+ # END for each blob
586
+
587
+ return self
588
+
589
+ def update(self) -> "IndexFile":
590
+ """Reread the contents of our index file, discarding all cached information
591
+ we might have.
592
+
593
+ :note:
594
+ This is a possibly dangerous operations as it will discard your changes to
595
+ :attr:`index.entries <entries>`.
596
+
597
+ :return:
598
+ self
599
+ """
600
+ self._delete_entries_cache()
601
+ # Allows to lazily reread on demand.
602
+ return self
603
+
604
+ def write_tree(self) -> Tree:
605
+ """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
606
+ into the repository's object database and return it.
607
+
608
+ :return:
609
+ :class:`~git.objects.tree.Tree` object representing this index.
610
+
611
+ :note:
612
+ The tree will be written even if one or more objects the tree refers to does
613
+ not yet exist in the object database. This could happen if you added entries
614
+ to the index directly.
615
+
616
+ :raise ValueError:
617
+ If there are no entries in the cache.
618
+
619
+ :raise git.exc.UnmergedEntriesError:
620
+ """
621
+ # We obtain no lock as we just flush our contents to disk as tree.
622
+ # If we are a new index, the entries access will load our data accordingly.
623
+ mdb = MemoryDB()
624
+ entries = self._entries_sorted()
625
+ binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
626
+
627
+ # Copy changed trees only.
628
+ mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
629
+
630
+ # Note: Additional deserialization could be saved if write_tree_from_cache would
631
+ # return sorted tree entries.
632
+ root_tree = Tree(self.repo, binsha, path="")
633
+ root_tree._cache = tree_items
634
+ return root_tree
635
+
636
+ def _process_diff_args(
637
+ self,
638
+ args: List[Union[PathLike, "git_diff.Diffable"]],
639
+ ) -> List[Union[PathLike, "git_diff.Diffable"]]:
640
+ try:
641
+ args.pop(args.index(self))
642
+ except IndexError:
643
+ pass
644
+ # END remove self
645
+ return args
646
+
647
+ def _to_relative_path(self, path: PathLike) -> PathLike:
648
+ """
649
+ :return:
650
+ Version of path relative to our git directory or raise :exc:`ValueError` if
651
+ it is not within our git directory.
652
+
653
+ :raise ValueError:
654
+ """
655
+ if not osp.isabs(path):
656
+ return path
657
+ if self.repo.bare:
658
+ raise InvalidGitRepositoryError("require non-bare repository")
659
+ if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
660
+ raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
661
+ result = os.path.relpath(path, self.repo.working_tree_dir)
662
+ if str(path).endswith(os.sep) and not result.endswith(os.sep):
663
+ result += os.sep
664
+ return result
665
+
666
+ def _preprocess_add_items(
667
+ self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
668
+ ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
669
+ """Split the items into two lists of path strings and BaseEntries."""
670
+ paths = []
671
+ entries = []
672
+ # if it is a string put in list
673
+ if isinstance(items, (str, os.PathLike)):
674
+ items = [items]
675
+
676
+ for item in items:
677
+ if isinstance(item, (str, os.PathLike)):
678
+ paths.append(self._to_relative_path(item))
679
+ elif isinstance(item, (Blob, Submodule)):
680
+ entries.append(BaseIndexEntry.from_blob(item))
681
+ elif isinstance(item, BaseIndexEntry):
682
+ entries.append(item)
683
+ else:
684
+ raise TypeError("Invalid Type: %r" % item)
685
+ # END for each item
686
+ return paths, entries
687
+
688
+ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
689
+ """Store file at filepath in the database and return the base index entry.
690
+
691
+ :note:
692
+ This needs the :func:`~git.index.util.git_working_dir` decorator active!
693
+ This must be ensured in the calling code.
694
+ """
695
+ st = os.lstat(filepath) # Handles non-symlinks as well.
696
+
697
+ if S_ISLNK(st.st_mode):
698
+ # In PY3, readlink is a string, but we need bytes.
699
+ # In PY2, it was just OS encoded bytes, we assumed UTF-8.
700
+ def open_stream() -> BinaryIO:
701
+ return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
702
+ else:
703
+
704
+ def open_stream() -> BinaryIO:
705
+ return open(filepath, "rb")
706
+
707
+ with open_stream() as stream:
708
+ fprogress(filepath, False, filepath)
709
+ istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
710
+ fprogress(filepath, True, filepath)
711
+ return BaseIndexEntry(
712
+ (
713
+ stat_mode_to_index_mode(st.st_mode),
714
+ istream.binsha,
715
+ 0,
716
+ to_native_path_linux(filepath),
717
+ )
718
+ )
719
+
720
+ @unbare_repo
721
+ @git_working_dir
722
+ def _entries_for_paths(
723
+ self,
724
+ paths: List[str],
725
+ path_rewriter: Union[Callable, None],
726
+ fprogress: Callable,
727
+ entries: List[BaseIndexEntry],
728
+ ) -> List[BaseIndexEntry]:
729
+ entries_added: List[BaseIndexEntry] = []
730
+ if path_rewriter:
731
+ for path in paths:
732
+ if osp.isabs(path):
733
+ abspath = path
734
+ gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
735
+ else:
736
+ gitrelative_path = path
737
+ if self.repo.working_tree_dir:
738
+ abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
739
+ # END obtain relative and absolute paths
740
+
741
+ blob = Blob(
742
+ self.repo,
743
+ Blob.NULL_BIN_SHA,
744
+ stat_mode_to_index_mode(os.stat(abspath).st_mode),
745
+ to_native_path_linux(gitrelative_path),
746
+ )
747
+ # TODO: variable undefined
748
+ entries.append(BaseIndexEntry.from_blob(blob))
749
+ # END for each path
750
+ del paths[:]
751
+ # END rewrite paths
752
+
753
+ # HANDLE PATHS
754
+ assert len(entries_added) == 0
755
+ for filepath in self._iter_expand_paths(paths):
756
+ entries_added.append(self._store_path(filepath, fprogress))
757
+ # END for each filepath
758
+ # END path handling
759
+ return entries_added
760
+
761
+ def add(
762
+ self,
763
+ items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
764
+ force: bool = True,
765
+ fprogress: Callable = lambda *args: None,
766
+ path_rewriter: Union[Callable[..., PathLike], None] = None,
767
+ write: bool = True,
768
+ write_extension_data: bool = False,
769
+ ) -> List[BaseIndexEntry]:
770
+ R"""Add files from the working tree, specific blobs, or
771
+ :class:`~git.index.typ.BaseIndexEntry`\s to the index.
772
+
773
+ :param items:
774
+ Multiple types of items are supported, types can be mixed within one call.
775
+ Different types imply a different handling. File paths may generally be
776
+ relative or absolute.
777
+
778
+ - path string
779
+
780
+ Strings denote a relative or absolute path into the repository pointing
781
+ to an existing file, e.g., ``CHANGES``, ``lib/myfile.ext``,
782
+ ``/home/gitrepo/lib/myfile.ext``.
783
+
784
+ Absolute paths must start with working tree directory of this index's
785
+ repository to be considered valid. For example, if it was initialized
786
+ with a non-normalized path, like ``/root/repo/../repo``, absolute paths
787
+ to be added must start with ``/root/repo/../repo``.
788
+
789
+ Paths provided like this must exist. When added, they will be written
790
+ into the object database.
791
+
792
+ PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
793
+ directories like ``lib``, which will add all the files within the
794
+ directory and subdirectories.
795
+
796
+ This equals a straight :manpage:`git-add(1)`.
797
+
798
+ They are added at stage 0.
799
+
800
+ - :class:`~git.objects.blob.Blob` or
801
+ :class:`~git.objects.submodule.base.Submodule` object
802
+
803
+ Blobs are added as they are assuming a valid mode is set.
804
+
805
+ The file they refer to may or may not exist in the file system, but must
806
+ be a path relative to our repository.
807
+
808
+ If their sha is null (40*0), their path must exist in the file system
809
+ relative to the git repository as an object will be created from the
810
+ data at the path.
811
+
812
+ The handling now very much equals the way string paths are processed,
813
+ except that the mode you have set will be kept. This allows you to
814
+ create symlinks by settings the mode respectively and writing the target
815
+ of the symlink directly into the file. This equals a default Linux
816
+ symlink which is not dereferenced automatically, except that it can be
817
+ created on filesystems not supporting it as well.
818
+
819
+ Please note that globs or directories are not allowed in
820
+ :class:`~git.objects.blob.Blob` objects.
821
+
822
+ They are added at stage 0.
823
+
824
+ - :class:`~git.index.typ.BaseIndexEntry` or type
825
+
826
+ Handling equals the one of :class:`~git.objects.blob.Blob` objects, but
827
+ the stage may be explicitly set. Please note that Index Entries require
828
+ binary sha's.
829
+
830
+ :param force:
831
+ **CURRENTLY INEFFECTIVE**
832
+ If ``True``, otherwise ignored or excluded files will be added anyway. As
833
+ opposed to the :manpage:`git-add(1)` command, we enable this flag by default
834
+ as the API user usually wants the item to be added even though they might be
835
+ excluded.
836
+
837
+ :param fprogress:
838
+ Function with signature ``f(path, done=False, item=item)`` called for each
839
+ path to be added, one time once it is about to be added where ``done=False``
840
+ and once after it was added where ``done=True``.
841
+
842
+ ``item`` is set to the actual item we handle, either a path or a
843
+ :class:`~git.index.typ.BaseIndexEntry`.
844
+
845
+ Please note that the processed path is not guaranteed to be present in the
846
+ index already as the index is currently being processed.
847
+
848
+ :param path_rewriter:
849
+ Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
850
+ for each passed entry which is the path to be actually recorded for the
851
+ object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
852
+ This allows you to write an index which is not identical to the layout of
853
+ the actual files on your hard-disk. If not ``None`` and `items` contain
854
+ plain paths, these paths will be converted to Entries beforehand and passed
855
+ to the path_rewriter. Please note that ``entry.path`` is relative to the git
856
+ repository.
857
+
858
+ :param write:
859
+ If ``True``, the index will be written once it was altered. Otherwise the
860
+ changes only exist in memory and are not available to git commands.
861
+
862
+ :param write_extension_data:
863
+ If ``True``, extension data will be written back to the index. This can lead
864
+ to issues in case it is containing the 'TREE' extension, which will cause
865
+ the :manpage:`git-commit(1)` command to write an old tree, instead of a new
866
+ one representing the now changed index.
867
+
868
+ This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
869
+ 'TREE' extension altogether. You should set it to ``True`` if you intend to
870
+ use :meth:`IndexFile.commit` exclusively while maintaining support for
871
+ third-party extensions. Besides that, you can usually safely ignore the
872
+ built-in extensions when using GitPython on repositories that are not
873
+ handled manually at all.
874
+
875
+ All current built-in extensions are listed here:
876
+ https://git-scm.com/docs/index-format
877
+
878
+ :return:
879
+ List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
880
+ just actually added.
881
+
882
+ :raise OSError:
883
+ If a supplied path did not exist. Please note that
884
+ :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
885
+ will be added even if their paths do not exist.
886
+ """
887
+ # Sort the entries into strings and Entries.
888
+ # Blobs are converted to entries automatically.
889
+ # Paths can be git-added. For everything else we use git-update-index.
890
+ paths, entries = self._preprocess_add_items(items)
891
+ entries_added: List[BaseIndexEntry] = []
892
+ # This code needs a working tree, so we try not to run it unless required.
893
+ # That way, we are OK on a bare repository as well.
894
+ # If there are no paths, the rewriter has nothing to do either.
895
+ if paths:
896
+ entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
897
+
898
+ # HANDLE ENTRIES
899
+ if entries:
900
+ null_mode_entries = [e for e in entries if e.mode == 0]
901
+ if null_mode_entries:
902
+ raise ValueError(
903
+ "At least one Entry has a null-mode - please use index.remove to remove files for clarity"
904
+ )
905
+ # END null mode should be remove
906
+
907
+ # HANDLE ENTRY OBJECT CREATION
908
+ # Create objects if required, otherwise go with the existing shas.
909
+ null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
910
+ if null_entries_indices:
911
+
912
+ @git_working_dir
913
+ def handle_null_entries(self: "IndexFile") -> None:
914
+ for ei in null_entries_indices:
915
+ null_entry = entries[ei]
916
+ new_entry = self._store_path(null_entry.path, fprogress)
917
+
918
+ # Update null entry.
919
+ entries[ei] = BaseIndexEntry(
920
+ (
921
+ null_entry.mode,
922
+ new_entry.binsha,
923
+ null_entry.stage,
924
+ null_entry.path,
925
+ )
926
+ )
927
+ # END for each entry index
928
+
929
+ # END closure
930
+
931
+ handle_null_entries(self)
932
+ # END null_entry handling
933
+
934
+ # REWRITE PATHS
935
+ # If we have to rewrite the entries, do so now, after we have generated all
936
+ # object sha's.
937
+ if path_rewriter:
938
+ for i, e in enumerate(entries):
939
+ entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
940
+ # END for each entry
941
+ # END handle path rewriting
942
+
943
+ # Just go through the remaining entries and provide progress info.
944
+ for i, entry in enumerate(entries):
945
+ progress_sent = i in null_entries_indices
946
+ if not progress_sent:
947
+ fprogress(entry.path, False, entry)
948
+ fprogress(entry.path, True, entry)
949
+ # END handle progress
950
+ # END for each entry
951
+ entries_added.extend(entries)
952
+ # END if there are base entries
953
+
954
+ # FINALIZE
955
+ # Add the new entries to this instance.
956
+ for entry in entries_added:
957
+ self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
958
+
959
+ if write:
960
+ self.write(ignore_extension_data=not write_extension_data)
961
+ # END handle write
962
+
963
+ return entries_added
964
+
965
+ def _items_to_rela_paths(
966
+ self,
967
+ items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
968
+ ) -> List[PathLike]:
969
+ """Returns a list of repo-relative paths from the given items which
970
+ may be absolute or relative paths, entries or blobs."""
971
+ paths = []
972
+ # If string, put in list.
973
+ if isinstance(items, (str, os.PathLike)):
974
+ items = [items]
975
+
976
+ for item in items:
977
+ if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
978
+ paths.append(self._to_relative_path(item.path))
979
+ elif isinstance(item, (str, os.PathLike)):
980
+ paths.append(self._to_relative_path(item))
981
+ else:
982
+ raise TypeError("Invalid item type: %r" % item)
983
+ # END for each item
984
+ return paths
985
+
986
+ @post_clear_cache
987
+ @default_index
988
+ def remove(
989
+ self,
990
+ items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
991
+ working_tree: bool = False,
992
+ **kwargs: Any,
993
+ ) -> List[str]:
994
+ R"""Remove the given items from the index and optionally from the working tree
995
+ as well.
996
+
997
+ :param items:
998
+ Multiple types of items are supported which may be be freely mixed.
999
+
1000
+ - path string
1001
+
1002
+ Remove the given path at all stages. If it is a directory, you must
1003
+ specify the ``r=True`` keyword argument to remove all file entries below
1004
+ it. If absolute paths are given, they will be converted to a path
1005
+ relative to the git repository directory containing the working tree
1006
+
1007
+ The path string may include globs, such as ``*.c``.
1008
+
1009
+ - :class:`~git.objects.blob.Blob` object
1010
+
1011
+ Only the path portion is used in this case.
1012
+
1013
+ - :class:`~git.index.typ.BaseIndexEntry` or compatible type
1014
+
1015
+ The only relevant information here is the path. The stage is ignored.
1016
+
1017
+ :param working_tree:
1018
+ If ``True``, the entry will also be removed from the working tree,
1019
+ physically removing the respective file. This may fail if there are
1020
+ uncommitted changes in it.
1021
+
1022
+ :param kwargs:
1023
+ Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1024
+ ``r`` to allow recursive removal.
1025
+
1026
+ :return:
1027
+ List(path_string, ...) list of repository relative paths that have been
1028
+ removed effectively.
1029
+
1030
+ This is interesting to know in case you have provided a directory or globs.
1031
+ Paths are relative to the repository.
1032
+ """
1033
+ args = []
1034
+ if not working_tree:
1035
+ args.append("--cached")
1036
+ args.append("--")
1037
+
1038
+ # Preprocess paths.
1039
+ paths = self._items_to_rela_paths(items)
1040
+ removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1041
+
1042
+ # Process output to gain proper paths.
1043
+ # rm 'path'
1044
+ return [p[4:-1] for p in removed_paths]
1045
+
1046
+ @post_clear_cache
1047
+ @default_index
1048
+ def move(
1049
+ self,
1050
+ items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
1051
+ skip_errors: bool = False,
1052
+ **kwargs: Any,
1053
+ ) -> List[Tuple[str, str]]:
1054
+ """Rename/move the items, whereas the last item is considered the destination of
1055
+ the move operation.
1056
+
1057
+ If the destination is a file, the first item (of two) must be a file as well.
1058
+
1059
+ If the destination is a directory, it may be preceded by one or more directories
1060
+ or files.
1061
+
1062
+ The working tree will be affected in non-bare repositories.
1063
+
1064
+ :param items:
1065
+ Multiple types of items are supported, please see the :meth:`remove` method
1066
+ for reference.
1067
+
1068
+ :param skip_errors:
1069
+ If ``True``, errors such as ones resulting from missing source files will be
1070
+ skipped.
1071
+
1072
+ :param kwargs:
1073
+ Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
1074
+ ``dry_run`` or ``force``.
1075
+
1076
+ :return:
1077
+ List(tuple(source_path_string, destination_path_string), ...)
1078
+
1079
+ A list of pairs, containing the source file moved as well as its actual
1080
+ destination. Relative to the repository root.
1081
+
1082
+ :raise ValueError:
1083
+ If only one item was given.
1084
+
1085
+ :raise git.exc.GitCommandError:
1086
+ If git could not handle your request.
1087
+ """
1088
+ args = []
1089
+ if skip_errors:
1090
+ args.append("-k")
1091
+
1092
+ paths = self._items_to_rela_paths(items)
1093
+ if len(paths) < 2:
1094
+ raise ValueError("Please provide at least one source and one destination of the move operation")
1095
+
1096
+ was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
1097
+ kwargs["dry_run"] = True
1098
+
1099
+ # First execute rename in dry run so the command tells us what it actually does
1100
+ # (for later output).
1101
+ out = []
1102
+ mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1103
+
1104
+ # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
1105
+ # 'renaming' ones which we parse.
1106
+ for ln in range(int(len(mvlines) / 2), len(mvlines)):
1107
+ tokens = mvlines[ln].split(" to ")
1108
+ assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1109
+
1110
+ # [0] = Renaming x
1111
+ # [1] = y
1112
+ out.append((tokens[0][9:], tokens[1]))
1113
+ # END for each line to parse
1114
+
1115
+ # Either prepare for the real run, or output the dry-run result.
1116
+ if was_dry_run:
1117
+ return out
1118
+ # END handle dry run
1119
+
1120
+ # Now apply the actual operation.
1121
+ kwargs.pop("dry_run")
1122
+ self.repo.git.mv(args, paths, **kwargs)
1123
+
1124
+ return out
1125
+
1126
+ def commit(
1127
+ self,
1128
+ message: str,
1129
+ parent_commits: Union[List[Commit], None] = None,
1130
+ head: bool = True,
1131
+ author: Union[None, Actor] = None,
1132
+ committer: Union[None, Actor] = None,
1133
+ author_date: Union[datetime.datetime, str, None] = None,
1134
+ commit_date: Union[datetime.datetime, str, None] = None,
1135
+ skip_hooks: bool = False,
1136
+ ) -> Commit:
1137
+ """Commit the current default index file, creating a
1138
+ :class:`~git.objects.commit.Commit` object.
1139
+
1140
+ For more information on the arguments, see
1141
+ :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
1142
+
1143
+ :note:
1144
+ If you have manually altered the :attr:`entries` member of this instance,
1145
+ don't forget to :meth:`write` your changes to disk beforehand.
1146
+
1147
+ :note:
1148
+ Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
1149
+ ``--no-verify`` on the command line.
1150
+
1151
+ :return:
1152
+ :class:`~git.objects.commit.Commit` object representing the new commit
1153
+ """
1154
+ if not skip_hooks:
1155
+ run_commit_hook("pre-commit", self)
1156
+
1157
+ self._write_commit_editmsg(message)
1158
+ run_commit_hook("commit-msg", self, self._commit_editmsg_filepath())
1159
+ message = self._read_commit_editmsg()
1160
+ self._remove_commit_editmsg()
1161
+ tree = self.write_tree()
1162
+ rval = Commit.create_from_tree(
1163
+ self.repo,
1164
+ tree,
1165
+ message,
1166
+ parent_commits,
1167
+ head,
1168
+ author=author,
1169
+ committer=committer,
1170
+ author_date=author_date,
1171
+ commit_date=commit_date,
1172
+ )
1173
+ if not skip_hooks:
1174
+ run_commit_hook("post-commit", self)
1175
+ return rval
1176
+
1177
+ def _write_commit_editmsg(self, message: str) -> None:
1178
+ with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file:
1179
+ commit_editmsg_file.write(message.encode(defenc))
1180
+
1181
+ def _remove_commit_editmsg(self) -> None:
1182
+ os.remove(self._commit_editmsg_filepath())
1183
+
1184
+ def _read_commit_editmsg(self) -> str:
1185
+ with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file:
1186
+ return commit_editmsg_file.read().decode(defenc)
1187
+
1188
+ def _commit_editmsg_filepath(self) -> str:
1189
+ return osp.join(self.repo.common_dir, "COMMIT_EDITMSG")
1190
+
1191
+ def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes:
1192
+ stdin_IO = proc.stdin
1193
+ if stdin_IO:
1194
+ stdin_IO.flush()
1195
+ stdin_IO.close()
1196
+
1197
+ stdout = b""
1198
+ if not ignore_stdout and proc.stdout:
1199
+ stdout = proc.stdout.read()
1200
+
1201
+ if proc.stdout:
1202
+ proc.stdout.close()
1203
+ proc.wait()
1204
+ return stdout
1205
+
1206
+ @default_index
1207
+ def checkout(
1208
+ self,
1209
+ paths: Union[None, Iterable[PathLike]] = None,
1210
+ force: bool = False,
1211
+ fprogress: Callable = lambda *args: None,
1212
+ **kwargs: Any,
1213
+ ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1214
+ """Check out the given paths or all files from the version known to the index
1215
+ into the working tree.
1216
+
1217
+ :note:
1218
+ Be sure you have written pending changes using the :meth:`write` method in
1219
+ case you have altered the entries dictionary directly.
1220
+
1221
+ :param paths:
1222
+ If ``None``, all paths in the index will be checked out.
1223
+ Otherwise an iterable of relative or absolute paths or a single path
1224
+ pointing to files or directories in the index is expected.
1225
+
1226
+ :param force:
1227
+ If ``True``, existing files will be overwritten even if they contain local
1228
+ modifications.
1229
+ If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
1230
+
1231
+ :param fprogress:
1232
+ See :meth:`IndexFile.add` for signature and explanation.
1233
+
1234
+ The provided progress information will contain ``None`` as path and item if
1235
+ no explicit paths are given. Otherwise progress information will be send
1236
+ prior and after a file has been checked out.
1237
+
1238
+ :param kwargs:
1239
+ Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1240
+
1241
+ :return:
1242
+ Iterable yielding paths to files which have been checked out and are
1243
+ guaranteed to match the version stored in the index.
1244
+
1245
+ :raise git.exc.CheckoutError:
1246
+ * If at least one file failed to be checked out. This is a summary, hence it
1247
+ will checkout as many files as it can anyway.
1248
+ * If one of files or directories do not exist in the index (as opposed to
1249
+ the original git command, which ignores them).
1250
+
1251
+ :raise git.exc.GitCommandError:
1252
+ If error lines could not be parsed - this truly is an exceptional state.
1253
+
1254
+ :note:
1255
+ The checkout is limited to checking out the files in the index. Files which
1256
+ are not in the index anymore and exist in the working tree will not be
1257
+ deleted. This behaviour is fundamentally different to ``head.checkout``,
1258
+ i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1259
+ ``head.checkout`` instead of ``index.checkout``.
1260
+ """
1261
+ args = ["--index"]
1262
+ if force:
1263
+ args.append("--force")
1264
+
1265
+ failed_files = []
1266
+ failed_reasons = []
1267
+ unknown_lines = []
1268
+
1269
+ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None:
1270
+ stderr_IO = proc.stderr
1271
+ if not stderr_IO:
1272
+ return # Return early if stderr empty.
1273
+
1274
+ stderr_bytes = stderr_IO.read()
1275
+ # line contents:
1276
+ stderr = stderr_bytes.decode(defenc)
1277
+ # git-checkout-index: this already exists
1278
+ endings = (
1279
+ " already exists",
1280
+ " is not in the cache",
1281
+ " does not exist at stage",
1282
+ " is unmerged",
1283
+ )
1284
+ for line in stderr.splitlines():
1285
+ if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
1286
+ is_a_dir = " is a directory"
1287
+ unlink_issue = "unable to unlink old '"
1288
+ already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...)
1289
+ if line.endswith(is_a_dir):
1290
+ failed_files.append(line[: -len(is_a_dir)])
1291
+ failed_reasons.append(is_a_dir)
1292
+ elif line.startswith(unlink_issue):
1293
+ failed_files.append(line[len(unlink_issue) : line.rfind("'")])
1294
+ failed_reasons.append(unlink_issue)
1295
+ elif line.endswith(already_exists_issue):
1296
+ failed_files.append(line[: -len(already_exists_issue)])
1297
+ failed_reasons.append(already_exists_issue)
1298
+ else:
1299
+ unknown_lines.append(line)
1300
+ continue
1301
+ # END special lines parsing
1302
+
1303
+ for e in endings:
1304
+ if line.endswith(e):
1305
+ failed_files.append(line[20 : -len(e)])
1306
+ failed_reasons.append(e)
1307
+ break
1308
+ # END if ending matches
1309
+ # END for each possible ending
1310
+ # END for each line
1311
+ if unknown_lines:
1312
+ raise GitCommandError(("git-checkout-index",), 128, stderr)
1313
+ if failed_files:
1314
+ valid_files = list(set(iter_checked_out_files) - set(failed_files))
1315
+ raise CheckoutError(
1316
+ "Some files could not be checked out from the index due to local modifications",
1317
+ failed_files,
1318
+ valid_files,
1319
+ failed_reasons,
1320
+ )
1321
+
1322
+ # END stderr handler
1323
+
1324
+ if paths is None:
1325
+ args.append("--all")
1326
+ kwargs["as_process"] = 1
1327
+ fprogress(None, False, None)
1328
+ proc = self.repo.git.checkout_index(*args, **kwargs)
1329
+ proc.wait()
1330
+ fprogress(None, True, None)
1331
+ rval_iter = (e.path for e in self.entries.values())
1332
+ handle_stderr(proc, rval_iter)
1333
+ return rval_iter
1334
+ else:
1335
+ if isinstance(paths, str):
1336
+ paths = [paths]
1337
+
1338
+ # Make sure we have our entries loaded before we start checkout_index, which
1339
+ # will hold a lock on it. We try to get the lock as well during our entries
1340
+ # initialization.
1341
+ self.entries # noqa: B018
1342
+
1343
+ args.append("--stdin")
1344
+ kwargs["as_process"] = True
1345
+ kwargs["istream"] = subprocess.PIPE
1346
+ proc = self.repo.git.checkout_index(args, **kwargs)
1347
+
1348
+ # FIXME: Reading from GIL!
1349
+ def make_exc() -> GitCommandError:
1350
+ return GitCommandError(("git-checkout-index", *args), 128, proc.stderr.read())
1351
+
1352
+ checked_out_files: List[PathLike] = []
1353
+
1354
+ for path in paths:
1355
+ co_path = to_native_path_linux(self._to_relative_path(path))
1356
+ # If the item is not in the index, it could be a directory.
1357
+ path_is_directory = False
1358
+
1359
+ try:
1360
+ self.entries[(co_path, 0)]
1361
+ except KeyError:
1362
+ folder = str(co_path)
1363
+ if not folder.endswith("/"):
1364
+ folder += "/"
1365
+ for entry in self.entries.values():
1366
+ if str(entry.path).startswith(folder):
1367
+ p = entry.path
1368
+ self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1369
+ checked_out_files.append(p)
1370
+ path_is_directory = True
1371
+ # END if entry is in directory
1372
+ # END for each entry
1373
+ # END path exception handlnig
1374
+
1375
+ if not path_is_directory:
1376
+ self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False)
1377
+ checked_out_files.append(co_path)
1378
+ # END path is a file
1379
+ # END for each path
1380
+ try:
1381
+ self._flush_stdin_and_wait(proc, ignore_stdout=True)
1382
+ except GitCommandError:
1383
+ # Without parsing stdout we don't know what failed.
1384
+ raise CheckoutError( # noqa: B904
1385
+ "Some files could not be checked out from the index, probably because they didn't exist.",
1386
+ failed_files,
1387
+ [],
1388
+ failed_reasons,
1389
+ )
1390
+
1391
+ handle_stderr(proc, checked_out_files)
1392
+ return checked_out_files
1393
+ # END paths handling
1394
+
1395
+ @default_index
1396
+ def reset(
1397
+ self,
1398
+ commit: Union[Commit, "Reference", str] = "HEAD",
1399
+ working_tree: bool = False,
1400
+ paths: Union[None, Iterable[PathLike]] = None,
1401
+ head: bool = False,
1402
+ **kwargs: Any,
1403
+ ) -> "IndexFile":
1404
+ """Reset the index to reflect the tree at the given commit. This will not adjust
1405
+ our HEAD reference by default, as opposed to
1406
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1407
+
1408
+ :param commit:
1409
+ Revision, :class:`~git.refs.reference.Reference` or
1410
+ :class:`~git.objects.commit.Commit` specifying the commit we should
1411
+ represent.
1412
+
1413
+ If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
1414
+ overwrite the default index.
1415
+
1416
+ :param working_tree:
1417
+ If ``True``, the files in the working tree will reflect the changed index.
1418
+ If ``False``, the working tree will not be touched.
1419
+ Please note that changes to the working copy will be discarded without
1420
+ warning!
1421
+
1422
+ :param head:
1423
+ If ``True``, the head will be set to the given commit. This is ``False`` by
1424
+ default, but if ``True``, this method behaves like
1425
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1426
+
1427
+ :param paths:
1428
+ If given as an iterable of absolute or repository-relative paths, only these
1429
+ will be reset to their state at the given commit-ish.
1430
+ The paths need to exist at the commit, otherwise an exception will be
1431
+ raised.
1432
+
1433
+ :param kwargs:
1434
+ Additional keyword arguments passed to :manpage:`git-reset(1)`.
1435
+
1436
+ :note:
1437
+ :meth:`IndexFile.reset`, as opposed to
1438
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
1439
+ order to maintain a consistent working tree. Instead, it will just check out
1440
+ the files according to their state in the index.
1441
+ If you want :manpage:`git-reset(1)`-like behaviour, use
1442
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
1443
+
1444
+ :return:
1445
+ self
1446
+ """
1447
+ # What we actually want to do is to merge the tree into our existing index,
1448
+ # which is what git-read-tree does.
1449
+ new_inst = type(self).from_tree(self.repo, commit)
1450
+ if not paths:
1451
+ self.entries = new_inst.entries
1452
+ else:
1453
+ nie = new_inst.entries
1454
+ for path in paths:
1455
+ path = self._to_relative_path(path)
1456
+ try:
1457
+ key = entry_key(path, 0)
1458
+ self.entries[key] = nie[key]
1459
+ except KeyError:
1460
+ # If key is not in theirs, it mustn't be in ours.
1461
+ try:
1462
+ del self.entries[key]
1463
+ except KeyError:
1464
+ pass
1465
+ # END handle deletion keyerror
1466
+ # END handle keyerror
1467
+ # END for each path
1468
+ # END handle paths
1469
+ self.write()
1470
+
1471
+ if working_tree:
1472
+ self.checkout(paths=paths, force=True)
1473
+ # END handle working tree
1474
+
1475
+ if head:
1476
+ self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit)
1477
+ # END handle head change
1478
+
1479
+ return self
1480
+
1481
+ # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1482
+ # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
1483
+ def diff(
1484
+ self,
1485
+ other: Union[ # type: ignore[override]
1486
+ Literal[git_diff.DiffConstants.INDEX],
1487
+ "Tree",
1488
+ "Commit",
1489
+ str,
1490
+ None,
1491
+ ] = git_diff.INDEX,
1492
+ paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
1493
+ create_patch: bool = False,
1494
+ **kwargs: Any,
1495
+ ) -> git_diff.DiffIndex[git_diff.Diff]:
1496
+ """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
1497
+ or :class:`~git.objects.commit.Commit` object.
1498
+
1499
+ For documentation of the parameters and return values, see
1500
+ :meth:`Diffable.diff <git.diff.Diffable.diff>`.
1501
+
1502
+ :note:
1503
+ Will only work with indices that represent the default git index as they
1504
+ have not been initialized with a stream.
1505
+ """
1506
+ # Only run if we are the default repository index.
1507
+ if self._file_path != self._index_path():
1508
+ raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
1509
+ # Index against index is always empty.
1510
+ if other is self.INDEX:
1511
+ return git_diff.DiffIndex()
1512
+
1513
+ # Index against anything but None is a reverse diff with the respective item.
1514
+ # Handle existing -R flags properly.
1515
+ # Transform strings to the object so that we can call diff on it.
1516
+ if isinstance(other, str):
1517
+ other = self.repo.rev_parse(other)
1518
+ # END object conversion
1519
+
1520
+ if isinstance(other, Object): # For Tree or Commit.
1521
+ # Invert the existing R flag.
1522
+ cur_val = kwargs.get("R", False)
1523
+ kwargs["R"] = not cur_val
1524
+ return other.diff(self.INDEX, paths, create_patch, **kwargs)
1525
+ # END diff against other item handling
1526
+
1527
+ # If other is not None here, something is wrong.
1528
+ if other is not None:
1529
+ raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
1530
+
1531
+ # Diff against working copy - can be handled by superclass natively.
1532
+ return super().diff(other, paths, create_patch, **kwargs)
.venv/lib/python3.11/site-packages/git/index/fun.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Standalone functions to accompany the index implementation and make it more
5
+ versatile."""
6
+
7
+ __all__ = [
8
+ "write_cache",
9
+ "read_cache",
10
+ "write_tree_from_cache",
11
+ "entry_key",
12
+ "stat_mode_to_index_mode",
13
+ "S_IFGITLINK",
14
+ "run_commit_hook",
15
+ "hook_path",
16
+ ]
17
+
18
+ from io import BytesIO
19
+ import os
20
+ import os.path as osp
21
+ from pathlib import Path
22
+ from stat import S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_ISDIR, S_ISLNK, S_IXUSR
23
+ import subprocess
24
+ import sys
25
+
26
+ from gitdb.base import IStream
27
+ from gitdb.typ import str_tree_type
28
+
29
+ from git.cmd import handle_process_output, safer_popen
30
+ from git.compat import defenc, force_bytes, force_text, safe_decode
31
+ from git.exc import HookExecutionError, UnmergedEntriesError
32
+ from git.objects.fun import (
33
+ traverse_tree_recursive,
34
+ traverse_trees_recursive,
35
+ tree_to_stream,
36
+ )
37
+ from git.util import IndexFileSHA1Writer, finalize_process
38
+
39
+ from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
40
+ from .util import pack, unpack
41
+
42
+ # typing -----------------------------------------------------------------------------
43
+
44
+ from typing import Dict, IO, List, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast
45
+
46
+ from git.types import PathLike
47
+
48
+ if TYPE_CHECKING:
49
+ from git.db import GitCmdObjectDB
50
+ from git.objects.tree import TreeCacheTup
51
+
52
+ from .base import IndexFile
53
+
54
+ # ------------------------------------------------------------------------------------
55
+
56
+ S_IFGITLINK = S_IFLNK | S_IFDIR
57
+ """Flags for a submodule."""
58
+
59
+ CE_NAMEMASK_INV = ~CE_NAMEMASK
60
+
61
+
62
+ def hook_path(name: str, git_dir: PathLike) -> str:
63
+ """:return: path to the given named hook in the given git repository directory"""
64
+ return osp.join(git_dir, "hooks", name)
65
+
66
+
67
+ def _has_file_extension(path: str) -> str:
68
+ return osp.splitext(path)[1]
69
+
70
+
71
+ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
72
+ """Run the commit hook of the given name. Silently ignore hooks that do not exist.
73
+
74
+ :param name:
75
+ Name of hook, like ``pre-commit``.
76
+
77
+ :param index:
78
+ :class:`~git.index.base.IndexFile` instance.
79
+
80
+ :param args:
81
+ Arguments passed to hook file.
82
+
83
+ :raise git.exc.HookExecutionError:
84
+ """
85
+ hp = hook_path(name, index.repo.git_dir)
86
+ if not os.access(hp, os.X_OK):
87
+ return
88
+
89
+ env = os.environ.copy()
90
+ env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
91
+ env["GIT_EDITOR"] = ":"
92
+ cmd = [hp]
93
+ try:
94
+ if sys.platform == "win32" and not _has_file_extension(hp):
95
+ # Windows only uses extensions to determine how to open files
96
+ # (doesn't understand shebangs). Try using bash to run the hook.
97
+ relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
98
+ cmd = ["bash.exe", relative_hp]
99
+
100
+ process = safer_popen(
101
+ cmd + list(args),
102
+ env=env,
103
+ stdout=subprocess.PIPE,
104
+ stderr=subprocess.PIPE,
105
+ cwd=index.repo.working_dir,
106
+ )
107
+ except Exception as ex:
108
+ raise HookExecutionError(hp, ex) from ex
109
+ else:
110
+ stdout_list: List[str] = []
111
+ stderr_list: List[str] = []
112
+ handle_process_output(process, stdout_list.append, stderr_list.append, finalize_process)
113
+ stdout = "".join(stdout_list)
114
+ stderr = "".join(stderr_list)
115
+ if process.returncode != 0:
116
+ stdout = force_text(stdout, defenc)
117
+ stderr = force_text(stderr, defenc)
118
+ raise HookExecutionError(hp, process.returncode, stderr, stdout)
119
+ # END handle return code
120
+
121
+
122
+ def stat_mode_to_index_mode(mode: int) -> int:
123
+ """Convert the given mode from a stat call to the corresponding index mode and
124
+ return it."""
125
+ if S_ISLNK(mode): # symlinks
126
+ return S_IFLNK
127
+ if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK: # submodules
128
+ return S_IFGITLINK
129
+ return S_IFREG | (mode & S_IXUSR and 0o755 or 0o644) # blobs with or without executable bit
130
+
131
+
132
+ def write_cache(
133
+ entries: Sequence[Union[BaseIndexEntry, "IndexEntry"]],
134
+ stream: IO[bytes],
135
+ extension_data: Union[None, bytes] = None,
136
+ ShaStreamCls: Type[IndexFileSHA1Writer] = IndexFileSHA1Writer,
137
+ ) -> None:
138
+ """Write the cache represented by entries to a stream.
139
+
140
+ :param entries:
141
+ **Sorted** list of entries.
142
+
143
+ :param stream:
144
+ Stream to wrap into the AdapterStreamCls - it is used for final output.
145
+
146
+ :param ShaStreamCls:
147
+ Type to use when writing to the stream. It produces a sha while writing to it,
148
+ before the data is passed on to the wrapped stream.
149
+
150
+ :param extension_data:
151
+ Any kind of data to write as a trailer, it must begin a 4 byte identifier,
152
+ followed by its size (4 bytes).
153
+ """
154
+ # Wrap the stream into a compatible writer.
155
+ stream_sha = ShaStreamCls(stream)
156
+
157
+ tell = stream_sha.tell
158
+ write = stream_sha.write
159
+
160
+ # Header
161
+ version = 2
162
+ write(b"DIRC")
163
+ write(pack(">LL", version, len(entries)))
164
+
165
+ # Body
166
+ for entry in entries:
167
+ beginoffset = tell()
168
+ write(entry.ctime_bytes) # ctime
169
+ write(entry.mtime_bytes) # mtime
170
+ path_str = str(entry.path)
171
+ path: bytes = force_bytes(path_str, encoding=defenc)
172
+ plen = len(path) & CE_NAMEMASK # Path length
173
+ assert plen == len(path), "Path %s too long to fit into index" % entry.path
174
+ flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values.
175
+ write(
176
+ pack(
177
+ ">LLLLLL20sH",
178
+ entry.dev,
179
+ entry.inode,
180
+ entry.mode,
181
+ entry.uid,
182
+ entry.gid,
183
+ entry.size,
184
+ entry.binsha,
185
+ flags,
186
+ )
187
+ )
188
+ write(path)
189
+ real_size = (tell() - beginoffset + 8) & ~7
190
+ write(b"\0" * ((beginoffset + real_size) - tell()))
191
+ # END for each entry
192
+
193
+ # Write previously cached extensions data.
194
+ if extension_data is not None:
195
+ stream_sha.write(extension_data)
196
+
197
+ # Write the sha over the content.
198
+ stream_sha.write_sha()
199
+
200
+
201
+ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
202
+ """Return tuple(version_long, num_entries) from the given stream."""
203
+ type_id = stream.read(4)
204
+ if type_id != b"DIRC":
205
+ raise AssertionError("Invalid index file header: %r" % type_id)
206
+ unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2)))
207
+ version, num_entries = unpacked
208
+
209
+ # TODO: Handle version 3: extended data, see read-cache.c.
210
+ assert version in (1, 2), "Unsupported git index version %i, only 1 and 2 are supported" % version
211
+ return version, num_entries
212
+
213
+
214
+ def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]:
215
+ """
216
+ :return:
217
+ Key suitable to be used for the
218
+ :attr:`index.entries <git.index.base.IndexFile.entries>` dictionary.
219
+
220
+ :param entry:
221
+ One instance of type BaseIndexEntry or the path and the stage.
222
+ """
223
+
224
+ # def is_entry_key_tup(entry_key: Tuple) -> TypeGuard[Tuple[PathLike, int]]:
225
+ # return isinstance(entry_key, tuple) and len(entry_key) == 2
226
+
227
+ if len(entry) == 1:
228
+ entry_first = entry[0]
229
+ assert isinstance(entry_first, BaseIndexEntry)
230
+ return (entry_first.path, entry_first.stage)
231
+ else:
232
+ # assert is_entry_key_tup(entry)
233
+ entry = cast(Tuple[PathLike, int], entry)
234
+ return entry
235
+ # END handle entry
236
+
237
+
238
+ def read_cache(
239
+ stream: IO[bytes],
240
+ ) -> Tuple[int, Dict[Tuple[PathLike, int], "IndexEntry"], bytes, bytes]:
241
+ """Read a cache file from the given stream.
242
+
243
+ :return:
244
+ tuple(version, entries_dict, extension_data, content_sha)
245
+
246
+ * *version* is the integer version number.
247
+ * *entries_dict* is a dictionary which maps IndexEntry instances to a path at a
248
+ stage.
249
+ * *extension_data* is ``""`` or 4 bytes of type + 4 bytes of size + size bytes.
250
+ * *content_sha* is a 20 byte sha on all cache file contents.
251
+ """
252
+ version, num_entries = read_header(stream)
253
+ count = 0
254
+ entries: Dict[Tuple[PathLike, int], "IndexEntry"] = {}
255
+
256
+ read = stream.read
257
+ tell = stream.tell
258
+ while count < num_entries:
259
+ beginoffset = tell()
260
+ ctime = unpack(">8s", read(8))[0]
261
+ mtime = unpack(">8s", read(8))[0]
262
+ (dev, ino, mode, uid, gid, size, sha, flags) = unpack(">LLLLLL20sH", read(20 + 4 * 6 + 2))
263
+ path_size = flags & CE_NAMEMASK
264
+ path = read(path_size).decode(defenc)
265
+
266
+ real_size = (tell() - beginoffset + 8) & ~7
267
+ read((beginoffset + real_size) - tell())
268
+ entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size))
269
+ # entry_key would be the method to use, but we save the effort.
270
+ entries[(path, entry.stage)] = entry
271
+ count += 1
272
+ # END for each entry
273
+
274
+ # The footer contains extension data and a sha on the content so far.
275
+ # Keep the extension footer,and verify we have a sha in the end.
276
+ # Extension data format is:
277
+ # 4 bytes ID
278
+ # 4 bytes length of chunk
279
+ # Repeated 0 - N times
280
+ extension_data = stream.read(~0)
281
+ assert len(extension_data) > 19, (
282
+ "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data)
283
+ )
284
+
285
+ content_sha = extension_data[-20:]
286
+
287
+ # Truncate the sha in the end as we will dynamically create it anyway.
288
+ extension_data = extension_data[:-20]
289
+
290
+ return (version, entries, extension_data, content_sha)
291
+
292
+
293
+ def write_tree_from_cache(
294
+ entries: List[IndexEntry], odb: "GitCmdObjectDB", sl: slice, si: int = 0
295
+ ) -> Tuple[bytes, List["TreeCacheTup"]]:
296
+ R"""Create a tree from the given sorted list of entries and put the respective
297
+ trees into the given object database.
298
+
299
+ :param entries:
300
+ **Sorted** list of :class:`~git.index.typ.IndexEntry`\s.
301
+
302
+ :param odb:
303
+ Object database to store the trees in.
304
+
305
+ :param si:
306
+ Start index at which we should start creating subtrees.
307
+
308
+ :param sl:
309
+ Slice indicating the range we should process on the entries list.
310
+
311
+ :return:
312
+ tuple(binsha, list(tree_entry, ...))
313
+
314
+ A tuple of a sha and a list of tree entries being a tuple of hexsha, mode, name.
315
+ """
316
+ tree_items: List["TreeCacheTup"] = []
317
+
318
+ ci = sl.start
319
+ end = sl.stop
320
+ while ci < end:
321
+ entry = entries[ci]
322
+ if entry.stage != 0:
323
+ raise UnmergedEntriesError(entry)
324
+ # END abort on unmerged
325
+ ci += 1
326
+ rbound = entry.path.find("/", si)
327
+ if rbound == -1:
328
+ # It's not a tree.
329
+ tree_items.append((entry.binsha, entry.mode, entry.path[si:]))
330
+ else:
331
+ # Find common base range.
332
+ base = entry.path[si:rbound]
333
+ xi = ci
334
+ while xi < end:
335
+ oentry = entries[xi]
336
+ orbound = oentry.path.find("/", si)
337
+ if orbound == -1 or oentry.path[si:orbound] != base:
338
+ break
339
+ # END abort on base mismatch
340
+ xi += 1
341
+ # END find common base
342
+
343
+ # Enter recursion.
344
+ # ci - 1 as we want to count our current item as well.
345
+ sha, _tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)
346
+ tree_items.append((sha, S_IFDIR, base))
347
+
348
+ # Skip ahead.
349
+ ci = xi
350
+ # END handle bounds
351
+ # END for each entry
352
+
353
+ # Finally create the tree.
354
+ sio = BytesIO()
355
+ tree_to_stream(tree_items, sio.write) # Writes to stream as bytes, but doesn't change tree_items.
356
+ sio.seek(0)
357
+
358
+ istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio))
359
+ return (istream.binsha, tree_items)
360
+
361
+
362
+ def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> BaseIndexEntry:
363
+ return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2]))
364
+
365
+
366
+ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]:
367
+ R"""
368
+ :return:
369
+ List of :class:`~git.index.typ.BaseIndexEntry`\s representing the aggressive
370
+ merge of the given trees. All valid entries are on stage 0, whereas the
371
+ conflicting ones are left on stage 1, 2 or 3, whereas stage 1 corresponds to the
372
+ common ancestor tree, 2 to our tree and 3 to 'their' tree.
373
+
374
+ :param tree_shas:
375
+ 1, 2 or 3 trees as identified by their binary 20 byte shas. If 1 or two, the
376
+ entries will effectively correspond to the last given tree. If 3 are given, a 3
377
+ way merge is performed.
378
+ """
379
+ out: List[BaseIndexEntry] = []
380
+
381
+ # One and two way is the same for us, as we don't have to handle an existing
382
+ # index, instrea
383
+ if len(tree_shas) in (1, 2):
384
+ for entry in traverse_tree_recursive(odb, tree_shas[-1], ""):
385
+ out.append(_tree_entry_to_baseindexentry(entry, 0))
386
+ # END for each entry
387
+ return out
388
+ # END handle single tree
389
+
390
+ if len(tree_shas) > 3:
391
+ raise ValueError("Cannot handle %i trees at once" % len(tree_shas))
392
+
393
+ # Three trees.
394
+ for base, ours, theirs in traverse_trees_recursive(odb, tree_shas, ""):
395
+ if base is not None:
396
+ # Base version exists.
397
+ if ours is not None:
398
+ # Ours exists.
399
+ if theirs is not None:
400
+ # It exists in all branches. Ff it was changed in both
401
+ # its a conflict. Otherwise, we take the changed version.
402
+ # This should be the most common branch, so it comes first.
403
+ if (base[0] != ours[0] and base[0] != theirs[0] and ours[0] != theirs[0]) or (
404
+ base[1] != ours[1] and base[1] != theirs[1] and ours[1] != theirs[1]
405
+ ):
406
+ # Changed by both.
407
+ out.append(_tree_entry_to_baseindexentry(base, 1))
408
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
409
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
410
+ elif base[0] != ours[0] or base[1] != ours[1]:
411
+ # Only we changed it.
412
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
413
+ else:
414
+ # Either nobody changed it, or they did. In either
415
+ # case, use theirs.
416
+ out.append(_tree_entry_to_baseindexentry(theirs, 0))
417
+ # END handle modification
418
+ else:
419
+ if ours[0] != base[0] or ours[1] != base[1]:
420
+ # They deleted it, we changed it, conflict.
421
+ out.append(_tree_entry_to_baseindexentry(base, 1))
422
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
423
+ # else:
424
+ # # We didn't change it, ignore.
425
+ # pass
426
+ # END handle our change
427
+ # END handle theirs
428
+ else:
429
+ if theirs is None:
430
+ # Deleted in both, its fine - it's out.
431
+ pass
432
+ else:
433
+ if theirs[0] != base[0] or theirs[1] != base[1]:
434
+ # Deleted in ours, changed theirs, conflict.
435
+ out.append(_tree_entry_to_baseindexentry(base, 1))
436
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
437
+ # END theirs changed
438
+ # else:
439
+ # # Theirs didn't change.
440
+ # pass
441
+ # END handle theirs
442
+ # END handle ours
443
+ else:
444
+ # All three can't be None.
445
+ if ours is None:
446
+ # Added in their branch.
447
+ assert theirs is not None
448
+ out.append(_tree_entry_to_baseindexentry(theirs, 0))
449
+ elif theirs is None:
450
+ # Added in our branch.
451
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
452
+ else:
453
+ # Both have it, except for the base, see whether it changed.
454
+ if ours[0] != theirs[0] or ours[1] != theirs[1]:
455
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
456
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
457
+ else:
458
+ # It was added the same in both.
459
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
460
+ # END handle two items
461
+ # END handle heads
462
+ # END handle base exists
463
+ # END for each entries tuple
464
+
465
+ return out
.venv/lib/python3.11/site-packages/git/index/typ.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Additional types used by the index."""
5
+
6
+ __all__ = ["BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType"]
7
+
8
+ from binascii import b2a_hex
9
+ from pathlib import Path
10
+
11
+ from git.objects import Blob
12
+
13
+ from .util import pack, unpack
14
+
15
+ # typing ----------------------------------------------------------------------
16
+
17
+ from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
18
+
19
+ from git.types import PathLike
20
+
21
+ if TYPE_CHECKING:
22
+ from git.repo import Repo
23
+
24
+ StageType = int
25
+
26
+ # ---------------------------------------------------------------------------------
27
+
28
+ # { Invariants
29
+ CE_NAMEMASK = 0x0FFF
30
+ CE_STAGEMASK = 0x3000
31
+ CE_EXTENDED = 0x4000
32
+ CE_VALID = 0x8000
33
+ CE_STAGESHIFT = 12
34
+
35
+ # } END invariants
36
+
37
+
38
+ class BlobFilter:
39
+ """Predicate to be used by
40
+ :meth:`IndexFile.iter_blobs <git.index.base.IndexFile.iter_blobs>` allowing to
41
+ filter only return blobs which match the given list of directories or files.
42
+
43
+ The given paths are given relative to the repository.
44
+ """
45
+
46
+ __slots__ = ("paths",)
47
+
48
+ def __init__(self, paths: Sequence[PathLike]) -> None:
49
+ """
50
+ :param paths:
51
+ Tuple or list of paths which are either pointing to directories or to files
52
+ relative to the current repository.
53
+ """
54
+ self.paths = paths
55
+
56
+ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
57
+ blob_pathlike: PathLike = stage_blob[1].path
58
+ blob_path: Path = blob_pathlike if isinstance(blob_pathlike, Path) else Path(blob_pathlike)
59
+ for pathlike in self.paths:
60
+ path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike)
61
+ # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no
62
+ # longer supported.
63
+ filter_parts = path.parts
64
+ blob_parts = blob_path.parts
65
+ if len(filter_parts) > len(blob_parts):
66
+ continue
67
+ if all(i == j for i, j in zip(filter_parts, blob_parts)):
68
+ return True
69
+ return False
70
+
71
+
72
+ class BaseIndexEntryHelper(NamedTuple):
73
+ """Typed named tuple to provide named attribute access for :class:`BaseIndexEntry`.
74
+
75
+ This is needed to allow overriding ``__new__`` in child class to preserve backwards
76
+ compatibility.
77
+ """
78
+
79
+ mode: int
80
+ binsha: bytes
81
+ flags: int
82
+ path: PathLike
83
+ ctime_bytes: bytes = pack(">LL", 0, 0)
84
+ mtime_bytes: bytes = pack(">LL", 0, 0)
85
+ dev: int = 0
86
+ inode: int = 0
87
+ uid: int = 0
88
+ gid: int = 0
89
+ size: int = 0
90
+
91
+
92
+ class BaseIndexEntry(BaseIndexEntryHelper):
93
+ R"""Small brother of an index entry which can be created to describe changes
94
+ done to the index in which case plenty of additional information is not required.
95
+
96
+ As the first 4 data members match exactly to the :class:`IndexEntry` type, methods
97
+ expecting a :class:`BaseIndexEntry` can also handle full :class:`IndexEntry`\s even
98
+ if they use numeric indices for performance reasons.
99
+ """
100
+
101
+ def __new__(
102
+ cls,
103
+ inp_tuple: Union[
104
+ Tuple[int, bytes, int, PathLike],
105
+ Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
106
+ ],
107
+ ) -> "BaseIndexEntry":
108
+ """Override ``__new__`` to allow construction from a tuple for backwards
109
+ compatibility."""
110
+ return super().__new__(cls, *inp_tuple)
111
+
112
+ def __str__(self) -> str:
113
+ return "%o %s %i\t%s" % (self.mode, self.hexsha, self.stage, self.path)
114
+
115
+ def __repr__(self) -> str:
116
+ return "(%o, %s, %i, %s)" % (self.mode, self.hexsha, self.stage, self.path)
117
+
118
+ @property
119
+ def hexsha(self) -> str:
120
+ """hex version of our sha"""
121
+ return b2a_hex(self.binsha).decode("ascii")
122
+
123
+ @property
124
+ def stage(self) -> int:
125
+ """Stage of the entry, either:
126
+
127
+ * 0 = default stage
128
+ * 1 = stage before a merge or common ancestor entry in case of a 3 way merge
129
+ * 2 = stage of entries from the 'left' side of the merge
130
+ * 3 = stage of entries from the 'right' side of the merge
131
+
132
+ :note:
133
+ For more information, see :manpage:`git-read-tree(1)`.
134
+ """
135
+ return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
136
+
137
+ @classmethod
138
+ def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry":
139
+ """:return: Fully equipped BaseIndexEntry at the given stage"""
140
+ return cls((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path))
141
+
142
+ def to_blob(self, repo: "Repo") -> Blob:
143
+ """:return: Blob using the information of this index entry"""
144
+ return Blob(repo, self.binsha, self.mode, self.path)
145
+
146
+
147
+ class IndexEntry(BaseIndexEntry):
148
+ """Allows convenient access to index entry data as defined in
149
+ :class:`BaseIndexEntry` without completely unpacking it.
150
+
151
+ Attributes usually accessed often are cached in the tuple whereas others are
152
+ unpacked on demand.
153
+
154
+ See the properties for a mapping between names and tuple indices.
155
+ """
156
+
157
+ @property
158
+ def ctime(self) -> Tuple[int, int]:
159
+ """
160
+ :return:
161
+ Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
162
+ file's creation time
163
+ """
164
+ return cast(Tuple[int, int], unpack(">LL", self.ctime_bytes))
165
+
166
+ @property
167
+ def mtime(self) -> Tuple[int, int]:
168
+ """See :attr:`ctime` property, but returns modification time."""
169
+ return cast(Tuple[int, int], unpack(">LL", self.mtime_bytes))
170
+
171
+ @classmethod
172
+ def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry":
173
+ """
174
+ :return:
175
+ Minimal entry as created from the given :class:`BaseIndexEntry` instance.
176
+ Missing values will be set to null-like values.
177
+
178
+ :param base:
179
+ Instance of type :class:`BaseIndexEntry`.
180
+ """
181
+ time = pack(">LL", 0, 0)
182
+ return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0))
183
+
184
+ @classmethod
185
+ def from_blob(cls, blob: Blob, stage: int = 0) -> "IndexEntry":
186
+ """:return: Minimal entry resembling the given blob object"""
187
+ time = pack(">LL", 0, 0)
188
+ return IndexEntry(
189
+ (
190
+ blob.mode,
191
+ blob.binsha,
192
+ stage << CE_STAGESHIFT,
193
+ blob.path,
194
+ time,
195
+ time,
196
+ 0,
197
+ 0,
198
+ 0,
199
+ 0,
200
+ blob.size,
201
+ )
202
+ )
.venv/lib/python3.11/site-packages/git/index/util.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Index utilities."""
5
+
6
+ __all__ = ["TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir"]
7
+
8
+ import contextlib
9
+ from functools import wraps
10
+ import os
11
+ import os.path as osp
12
+ import struct
13
+ import tempfile
14
+ from types import TracebackType
15
+
16
+ # typing ----------------------------------------------------------------------
17
+
18
+ from typing import Any, Callable, TYPE_CHECKING, Optional, Type
19
+
20
+ from git.types import Literal, PathLike, _T
21
+
22
+ if TYPE_CHECKING:
23
+ from git.index import IndexFile
24
+
25
+ # ---------------------------------------------------------------------------------
26
+
27
+ # { Aliases
28
+ pack = struct.pack
29
+ unpack = struct.unpack
30
+ # } END aliases
31
+
32
+
33
+ class TemporaryFileSwap:
34
+ """Utility class moving a file to a temporary location within the same directory and
35
+ moving it back on to where on object deletion."""
36
+
37
+ __slots__ = ("file_path", "tmp_file_path")
38
+
39
+ def __init__(self, file_path: PathLike) -> None:
40
+ self.file_path = file_path
41
+ dirname, basename = osp.split(file_path)
42
+ fd, self.tmp_file_path = tempfile.mkstemp(prefix=basename, dir=dirname)
43
+ os.close(fd)
44
+ with contextlib.suppress(OSError): # It may be that the source does not exist.
45
+ os.replace(self.file_path, self.tmp_file_path)
46
+
47
+ def __enter__(self) -> "TemporaryFileSwap":
48
+ return self
49
+
50
+ def __exit__(
51
+ self,
52
+ exc_type: Optional[Type[BaseException]],
53
+ exc_val: Optional[BaseException],
54
+ exc_tb: Optional[TracebackType],
55
+ ) -> Literal[False]:
56
+ if osp.isfile(self.tmp_file_path):
57
+ os.replace(self.tmp_file_path, self.file_path)
58
+ return False
59
+
60
+
61
+ # { Decorators
62
+
63
+
64
+ def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
65
+ """Decorator for functions that alter the index using the git command.
66
+
67
+ When a git command alters the index, this invalidates our possibly existing entries
68
+ dictionary, which is why it must be deleted to allow it to be lazily reread later.
69
+ """
70
+
71
+ @wraps(func)
72
+ def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
73
+ rval = func(self, *args, **kwargs)
74
+ self._delete_entries_cache()
75
+ return rval
76
+
77
+ # END wrapper method
78
+
79
+ return post_clear_cache_if_not_raised
80
+
81
+
82
+ def default_index(func: Callable[..., _T]) -> Callable[..., _T]:
83
+ """Decorator ensuring the wrapped method may only run if we are the default
84
+ repository index.
85
+
86
+ This is as we rely on git commands that operate on that index only.
87
+ """
88
+
89
+ @wraps(func)
90
+ def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
91
+ if self._file_path != self._index_path():
92
+ raise AssertionError(
93
+ "Cannot call %r on indices that do not represent the default git index" % func.__name__
94
+ )
95
+ return func(self, *args, **kwargs)
96
+
97
+ # END wrapper method
98
+
99
+ return check_default_index
100
+
101
+
102
+ def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
103
+ """Decorator which changes the current working dir to the one of the git
104
+ repository in order to ensure relative paths are handled correctly."""
105
+
106
+ @wraps(func)
107
+ def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
108
+ cur_wd = os.getcwd()
109
+ os.chdir(str(self.repo.working_tree_dir))
110
+ try:
111
+ return func(self, *args, **kwargs)
112
+ finally:
113
+ os.chdir(cur_wd)
114
+ # END handle working dir
115
+
116
+ # END wrapper
117
+
118
+ return set_git_working_dir
119
+
120
+
121
+ # } END decorators
.venv/lib/python3.11/site-packages/git/objects/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Import all submodules' main classes into the package space."""
5
+
6
+ __all__ = [
7
+ "IndexObject",
8
+ "Object",
9
+ "Blob",
10
+ "Commit",
11
+ "Submodule",
12
+ "UpdateProgress",
13
+ "RootModule",
14
+ "RootUpdateProgress",
15
+ "TagObject",
16
+ "Tree",
17
+ "TreeModifier",
18
+ ]
19
+
20
+ from .base import IndexObject, Object
21
+ from .blob import Blob
22
+ from .commit import Commit
23
+ from .submodule import RootModule, RootUpdateProgress, Submodule, UpdateProgress
24
+ from .tag import TagObject
25
+ from .tree import Tree, TreeModifier
.venv/lib/python3.11/site-packages/git/objects/base.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ __all__ = ["Object", "IndexObject"]
7
+
8
+ import os.path as osp
9
+
10
+ import gitdb.typ as dbtyp
11
+
12
+ from git.exc import WorkTreeRepositoryUnsupported
13
+ from git.util import LazyMixin, bin_to_hex, join_path_native, stream_copy
14
+
15
+ from .util import get_object_type_by_name
16
+
17
+ # typing ------------------------------------------------------------------
18
+
19
+ from typing import Any, TYPE_CHECKING, Union
20
+
21
+ from git.types import AnyGitObject, GitObjectTypeString, PathLike
22
+
23
+ if TYPE_CHECKING:
24
+ from gitdb.base import OStream
25
+
26
+ from git.refs.reference import Reference
27
+ from git.repo import Repo
28
+
29
+ from .blob import Blob
30
+ from .submodule.base import Submodule
31
+ from .tree import Tree
32
+
33
+ IndexObjUnion = Union["Tree", "Blob", "Submodule"]
34
+
35
+ # --------------------------------------------------------------------------
36
+
37
+
38
+ class Object(LazyMixin):
39
+ """Base class for classes representing git object types.
40
+
41
+ The following four leaf classes represent specific kinds of git objects:
42
+
43
+ * :class:`Blob <git.objects.blob.Blob>`
44
+ * :class:`Tree <git.objects.tree.Tree>`
45
+ * :class:`Commit <git.objects.commit.Commit>`
46
+ * :class:`TagObject <git.objects.tag.TagObject>`
47
+
48
+ See :manpage:`gitglossary(7)` on:
49
+
50
+ * "object": https://git-scm.com/docs/gitglossary#def_object
51
+ * "object type": https://git-scm.com/docs/gitglossary#def_object_type
52
+ * "blob": https://git-scm.com/docs/gitglossary#def_blob_object
53
+ * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
54
+ * "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
55
+ * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
56
+
57
+ :note:
58
+ See the :class:`~git.types.AnyGitObject` union type of the four leaf subclasses
59
+ that represent actual git object types.
60
+
61
+ :note:
62
+ :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
63
+ rooted at this :class:`Object` class, even though submodules are not really a
64
+ type of git object. (This also applies to its
65
+ :class:`~git.objects.submodule.root.RootModule` subclass.)
66
+
67
+ :note:
68
+ This :class:`Object` class should not be confused with :class:`object` (the root
69
+ of the class hierarchy in Python).
70
+ """
71
+
72
+ NULL_HEX_SHA = "0" * 40
73
+ NULL_BIN_SHA = b"\0" * 20
74
+
75
+ TYPES = (
76
+ dbtyp.str_blob_type,
77
+ dbtyp.str_tree_type,
78
+ dbtyp.str_commit_type,
79
+ dbtyp.str_tag_type,
80
+ )
81
+
82
+ __slots__ = ("repo", "binsha", "size")
83
+
84
+ type: Union[GitObjectTypeString, None] = None
85
+ """String identifying (a concrete :class:`Object` subtype for) a git object type.
86
+
87
+ The subtypes that this may name correspond to the kinds of git objects that exist,
88
+ i.e., the objects that may be present in a git repository.
89
+
90
+ :note:
91
+ Most subclasses represent specific types of git objects and override this class
92
+ attribute accordingly. This attribute is ``None`` in the :class:`Object` base
93
+ class, as well as the :class:`IndexObject` intermediate subclass, but never
94
+ ``None`` in concrete leaf subclasses representing specific git object types.
95
+
96
+ :note:
97
+ See also :class:`~git.types.GitObjectTypeString`.
98
+ """
99
+
100
+ def __init__(self, repo: "Repo", binsha: bytes) -> None:
101
+ """Initialize an object by identifying it by its binary sha.
102
+
103
+ All keyword arguments will be set on demand if ``None``.
104
+
105
+ :param repo:
106
+ Repository this object is located in.
107
+
108
+ :param binsha:
109
+ 20 byte SHA1
110
+ """
111
+ super().__init__()
112
+ self.repo = repo
113
+ self.binsha = binsha
114
+ assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (
115
+ binsha,
116
+ len(binsha),
117
+ )
118
+
119
+ @classmethod
120
+ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
121
+ """
122
+ :return:
123
+ New :class:`Object` instance of a type appropriate to the object type behind
124
+ `id`. The id of the newly created object will be a binsha even though the
125
+ input id may have been a :class:`~git.refs.reference.Reference` or rev-spec.
126
+
127
+ :param id:
128
+ :class:`~git.refs.reference.Reference`, rev-spec, or hexsha.
129
+
130
+ :note:
131
+ This cannot be a ``__new__`` method as it would always call :meth:`__init__`
132
+ with the input id which is not necessarily a binsha.
133
+ """
134
+ return repo.rev_parse(str(id))
135
+
136
+ @classmethod
137
+ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject:
138
+ """
139
+ :return:
140
+ New object instance of a type appropriate to represent the given binary sha1
141
+
142
+ :param sha1:
143
+ 20 byte binary sha1.
144
+ """
145
+ if sha1 == cls.NULL_BIN_SHA:
146
+ # The NULL binsha is always the root commit.
147
+ return get_object_type_by_name(b"commit")(repo, sha1)
148
+ # END handle special case
149
+ oinfo = repo.odb.info(sha1)
150
+ inst = get_object_type_by_name(oinfo.type)(repo, oinfo.binsha)
151
+ inst.size = oinfo.size
152
+ return inst
153
+
154
+ def _set_cache_(self, attr: str) -> None:
155
+ """Retrieve object information."""
156
+ if attr == "size":
157
+ oinfo = self.repo.odb.info(self.binsha)
158
+ self.size = oinfo.size # type: int
159
+ else:
160
+ super()._set_cache_(attr)
161
+
162
+ def __eq__(self, other: Any) -> bool:
163
+ """:return: ``True`` if the objects have the same SHA1"""
164
+ if not hasattr(other, "binsha"):
165
+ return False
166
+ return self.binsha == other.binsha
167
+
168
+ def __ne__(self, other: Any) -> bool:
169
+ """:return: ``True`` if the objects do not have the same SHA1"""
170
+ if not hasattr(other, "binsha"):
171
+ return True
172
+ return self.binsha != other.binsha
173
+
174
+ def __hash__(self) -> int:
175
+ """:return: Hash of our id allowing objects to be used in dicts and sets"""
176
+ return hash(self.binsha)
177
+
178
+ def __str__(self) -> str:
179
+ """:return: String of our SHA1 as understood by all git commands"""
180
+ return self.hexsha
181
+
182
+ def __repr__(self) -> str:
183
+ """:return: String with pythonic representation of our object"""
184
+ return '<git.%s "%s">' % (self.__class__.__name__, self.hexsha)
185
+
186
+ @property
187
+ def hexsha(self) -> str:
188
+ """:return: 40 byte hex version of our 20 byte binary sha"""
189
+ # b2a_hex produces bytes.
190
+ return bin_to_hex(self.binsha).decode("ascii")
191
+
192
+ @property
193
+ def data_stream(self) -> "OStream":
194
+ """
195
+ :return:
196
+ File-object compatible stream to the uncompressed raw data of the object
197
+
198
+ :note:
199
+ Returned streams must be read in order.
200
+ """
201
+ return self.repo.odb.stream(self.binsha)
202
+
203
+ def stream_data(self, ostream: "OStream") -> "Object":
204
+ """Write our data directly to the given output stream.
205
+
206
+ :param ostream:
207
+ File-object compatible stream object.
208
+
209
+ :return:
210
+ self
211
+ """
212
+ istream = self.repo.odb.stream(self.binsha)
213
+ stream_copy(istream, ostream)
214
+ return self
215
+
216
+
217
+ class IndexObject(Object):
218
+ """Base for all objects that can be part of the index file.
219
+
220
+ The classes representing git object types that can be part of the index file are
221
+ :class:`~git.objects.tree.Tree` and :class:`~git.objects.blob.Blob`. In addition,
222
+ :class:`~git.objects.submodule.base.Submodule`, which is not really a git object
223
+ type but can be part of an index file, is also a subclass.
224
+ """
225
+
226
+ __slots__ = ("path", "mode")
227
+
228
+ # For compatibility with iterable lists.
229
+ _id_attribute_ = "path"
230
+
231
+ def __init__(
232
+ self,
233
+ repo: "Repo",
234
+ binsha: bytes,
235
+ mode: Union[None, int] = None,
236
+ path: Union[None, PathLike] = None,
237
+ ) -> None:
238
+ """Initialize a newly instanced :class:`IndexObject`.
239
+
240
+ :param repo:
241
+ The :class:`~git.repo.base.Repo` we are located in.
242
+
243
+ :param binsha:
244
+ 20 byte sha1.
245
+
246
+ :param mode:
247
+ The stat-compatible file mode as :class:`int`.
248
+ Use the :mod:`stat` module to evaluate the information.
249
+
250
+ :param path:
251
+ The path to the file in the file system, relative to the git repository
252
+ root, like ``file.ext`` or ``folder/other.ext``.
253
+
254
+ :note:
255
+ Path may not be set if the index object has been created directly, as it
256
+ cannot be retrieved without knowing the parent tree.
257
+ """
258
+ super().__init__(repo, binsha)
259
+ if mode is not None:
260
+ self.mode = mode
261
+ if path is not None:
262
+ self.path = path
263
+
264
+ def __hash__(self) -> int:
265
+ """
266
+ :return:
267
+ Hash of our path as index items are uniquely identifiable by path, not by
268
+ their data!
269
+ """
270
+ return hash(self.path)
271
+
272
+ def _set_cache_(self, attr: str) -> None:
273
+ if attr in IndexObject.__slots__:
274
+ # They cannot be retrieved later on (not without searching for them).
275
+ raise AttributeError(
276
+ "Attribute '%s' unset: path and mode attributes must have been set during %s object creation"
277
+ % (attr, type(self).__name__)
278
+ )
279
+ else:
280
+ super()._set_cache_(attr)
281
+ # END handle slot attribute
282
+
283
+ @property
284
+ def name(self) -> str:
285
+ """:return: Name portion of the path, effectively being the basename"""
286
+ return osp.basename(self.path)
287
+
288
+ @property
289
+ def abspath(self) -> PathLike:
290
+ R"""
291
+ :return:
292
+ Absolute path to this index object in the file system (as opposed to the
293
+ :attr:`path` field which is a path relative to the git repository).
294
+
295
+ The returned path will be native to the system and contains ``\`` on
296
+ Windows.
297
+ """
298
+ if self.repo.working_tree_dir is not None:
299
+ return join_path_native(self.repo.working_tree_dir, self.path)
300
+ else:
301
+ raise WorkTreeRepositoryUnsupported("working_tree_dir was None or empty")
.venv/lib/python3.11/site-packages/git/objects/blob.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ __all__ = ["Blob"]
7
+
8
+ from mimetypes import guess_type
9
+ import sys
10
+
11
+ if sys.version_info >= (3, 8):
12
+ from typing import Literal
13
+ else:
14
+ from typing_extensions import Literal
15
+
16
+ from . import base
17
+
18
+
19
+ class Blob(base.IndexObject):
20
+ """A Blob encapsulates a git blob object.
21
+
22
+ See :manpage:`gitglossary(7)` on "blob":
23
+ https://git-scm.com/docs/gitglossary#def_blob_object
24
+ """
25
+
26
+ DEFAULT_MIME_TYPE = "text/plain"
27
+ type: Literal["blob"] = "blob"
28
+
29
+ # Valid blob modes
30
+ executable_mode = 0o100755
31
+ file_mode = 0o100644
32
+ link_mode = 0o120000
33
+
34
+ __slots__ = ()
35
+
36
+ @property
37
+ def mime_type(self) -> str:
38
+ """
39
+ :return:
40
+ String describing the mime type of this file (based on the filename)
41
+
42
+ :note:
43
+ Defaults to ``text/plain`` in case the actual file type is unknown.
44
+ """
45
+ guesses = None
46
+ if self.path:
47
+ guesses = guess_type(str(self.path))
48
+ return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
.venv/lib/python3.11/site-packages/git/objects/commit.py ADDED
@@ -0,0 +1,909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ __all__ = ["Commit"]
7
+
8
+ from collections import defaultdict
9
+ import datetime
10
+ from io import BytesIO
11
+ import logging
12
+ import os
13
+ import re
14
+ from subprocess import Popen, PIPE
15
+ import sys
16
+ from time import altzone, daylight, localtime, time, timezone
17
+ import warnings
18
+
19
+ from gitdb import IStream
20
+
21
+ from git.cmd import Git
22
+ from git.diff import Diffable
23
+ from git.util import Actor, Stats, finalize_process, hex_to_bin
24
+
25
+ from . import base
26
+ from .tree import Tree
27
+ from .util import (
28
+ Serializable,
29
+ TraversableIterableObj,
30
+ altz_to_utctz_str,
31
+ from_timestamp,
32
+ parse_actor_and_date,
33
+ parse_date,
34
+ )
35
+
36
+ # typing ------------------------------------------------------------------
37
+
38
+ from typing import (
39
+ Any,
40
+ Dict,
41
+ IO,
42
+ Iterator,
43
+ List,
44
+ Sequence,
45
+ Tuple,
46
+ TYPE_CHECKING,
47
+ Union,
48
+ cast,
49
+ )
50
+
51
+ if sys.version_info >= (3, 8):
52
+ from typing import Literal
53
+ else:
54
+ from typing_extensions import Literal
55
+
56
+ from git.types import PathLike
57
+
58
+ if TYPE_CHECKING:
59
+ from git.refs import SymbolicReference
60
+ from git.repo import Repo
61
+
62
+ # ------------------------------------------------------------------------
63
+
64
+ _logger = logging.getLogger(__name__)
65
+
66
+
67
+ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
68
+ """Wraps a git commit object.
69
+
70
+ See :manpage:`gitglossary(7)` on "commit object":
71
+ https://git-scm.com/docs/gitglossary#def_commit_object
72
+
73
+ :note:
74
+ This class will act lazily on some of its attributes and will query the value on
75
+ demand only if it involves calling the git binary.
76
+ """
77
+
78
+ # ENVIRONMENT VARIABLES
79
+ # Read when creating new commits.
80
+ env_author_date = "GIT_AUTHOR_DATE"
81
+ env_committer_date = "GIT_COMMITTER_DATE"
82
+
83
+ # CONFIGURATION KEYS
84
+ conf_encoding = "i18n.commitencoding"
85
+
86
+ # INVARIANTS
87
+ default_encoding = "UTF-8"
88
+
89
+ type: Literal["commit"] = "commit"
90
+
91
+ __slots__ = (
92
+ "tree",
93
+ "author",
94
+ "authored_date",
95
+ "author_tz_offset",
96
+ "committer",
97
+ "committed_date",
98
+ "committer_tz_offset",
99
+ "message",
100
+ "parents",
101
+ "encoding",
102
+ "gpgsig",
103
+ )
104
+
105
+ _id_attribute_ = "hexsha"
106
+
107
+ parents: Sequence["Commit"]
108
+
109
+ def __init__(
110
+ self,
111
+ repo: "Repo",
112
+ binsha: bytes,
113
+ tree: Union[Tree, None] = None,
114
+ author: Union[Actor, None] = None,
115
+ authored_date: Union[int, None] = None,
116
+ author_tz_offset: Union[None, float] = None,
117
+ committer: Union[Actor, None] = None,
118
+ committed_date: Union[int, None] = None,
119
+ committer_tz_offset: Union[None, float] = None,
120
+ message: Union[str, bytes, None] = None,
121
+ parents: Union[Sequence["Commit"], None] = None,
122
+ encoding: Union[str, None] = None,
123
+ gpgsig: Union[str, None] = None,
124
+ ) -> None:
125
+ """Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
126
+ default will be implicitly set on first query.
127
+
128
+ :param binsha:
129
+ 20 byte sha1.
130
+
131
+ :param tree:
132
+ A :class:`~git.objects.tree.Tree` object.
133
+
134
+ :param author:
135
+ The author :class:`~git.util.Actor` object.
136
+
137
+ :param authored_date: int_seconds_since_epoch
138
+ The authored DateTime - use :func:`time.gmtime` to convert it into a
139
+ different format.
140
+
141
+ :param author_tz_offset: int_seconds_west_of_utc
142
+ The timezone that the `authored_date` is in.
143
+
144
+ :param committer:
145
+ The committer string, as an :class:`~git.util.Actor` object.
146
+
147
+ :param committed_date: int_seconds_since_epoch
148
+ The committed DateTime - use :func:`time.gmtime` to convert it into a
149
+ different format.
150
+
151
+ :param committer_tz_offset: int_seconds_west_of_utc
152
+ The timezone that the `committed_date` is in.
153
+
154
+ :param message: string
155
+ The commit message.
156
+
157
+ :param encoding: string
158
+ Encoding of the message, defaults to UTF-8.
159
+
160
+ :param parents:
161
+ List or tuple of :class:`Commit` objects which are our parent(s) in the
162
+ commit dependency graph.
163
+
164
+ :return:
165
+ :class:`Commit`
166
+
167
+ :note:
168
+ Timezone information is in the same format and in the same sign as what
169
+ :func:`time.altzone` returns. The sign is inverted compared to git's UTC
170
+ timezone.
171
+ """
172
+ super().__init__(repo, binsha)
173
+ self.binsha = binsha
174
+ if tree is not None:
175
+ assert isinstance(tree, Tree), "Tree needs to be a Tree instance, was %s" % type(tree)
176
+ if tree is not None:
177
+ self.tree = tree
178
+ if author is not None:
179
+ self.author = author
180
+ if authored_date is not None:
181
+ self.authored_date = authored_date
182
+ if author_tz_offset is not None:
183
+ self.author_tz_offset = author_tz_offset
184
+ if committer is not None:
185
+ self.committer = committer
186
+ if committed_date is not None:
187
+ self.committed_date = committed_date
188
+ if committer_tz_offset is not None:
189
+ self.committer_tz_offset = committer_tz_offset
190
+ if message is not None:
191
+ self.message = message
192
+ if parents is not None:
193
+ self.parents = parents
194
+ if encoding is not None:
195
+ self.encoding = encoding
196
+ if gpgsig is not None:
197
+ self.gpgsig = gpgsig
198
+
199
+ @classmethod
200
+ def _get_intermediate_items(cls, commit: "Commit") -> Tuple["Commit", ...]:
201
+ return tuple(commit.parents)
202
+
203
+ @classmethod
204
+ def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes:
205
+ """Calculate the sha of a commit.
206
+
207
+ :param repo:
208
+ :class:`~git.repo.base.Repo` object the commit should be part of.
209
+
210
+ :param commit:
211
+ :class:`Commit` object for which to generate the sha.
212
+ """
213
+
214
+ stream = BytesIO()
215
+ commit._serialize(stream)
216
+ streamlen = stream.tell()
217
+ stream.seek(0)
218
+
219
+ istream = repo.odb.store(IStream(cls.type, streamlen, stream))
220
+ return istream.binsha
221
+
222
+ def replace(self, **kwargs: Any) -> "Commit":
223
+ """Create new commit object from an existing commit object.
224
+
225
+ Any values provided as keyword arguments will replace the corresponding
226
+ attribute in the new object.
227
+ """
228
+
229
+ attrs = {k: getattr(self, k) for k in self.__slots__}
230
+
231
+ for attrname in kwargs:
232
+ if attrname not in self.__slots__:
233
+ raise ValueError("invalid attribute name")
234
+
235
+ attrs.update(kwargs)
236
+ new_commit = self.__class__(self.repo, self.NULL_BIN_SHA, **attrs)
237
+ new_commit.binsha = self._calculate_sha_(self.repo, new_commit)
238
+
239
+ return new_commit
240
+
241
+ def _set_cache_(self, attr: str) -> None:
242
+ if attr in Commit.__slots__:
243
+ # Read the data in a chunk, its faster - then provide a file wrapper.
244
+ _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha)
245
+ self._deserialize(BytesIO(stream.read()))
246
+ else:
247
+ super()._set_cache_(attr)
248
+ # END handle attrs
249
+
250
+ @property
251
+ def authored_datetime(self) -> datetime.datetime:
252
+ return from_timestamp(self.authored_date, self.author_tz_offset)
253
+
254
+ @property
255
+ def committed_datetime(self) -> datetime.datetime:
256
+ return from_timestamp(self.committed_date, self.committer_tz_offset)
257
+
258
+ @property
259
+ def summary(self) -> Union[str, bytes]:
260
+ """:return: First line of the commit message"""
261
+ if isinstance(self.message, str):
262
+ return self.message.split("\n", 1)[0]
263
+ else:
264
+ return self.message.split(b"\n", 1)[0]
265
+
266
+ def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> int:
267
+ """Count the number of commits reachable from this commit.
268
+
269
+ :param paths:
270
+ An optional path or a list of paths restricting the return value to commits
271
+ actually containing the paths.
272
+
273
+ :param kwargs:
274
+ Additional options to be passed to :manpage:`git-rev-list(1)`. They must not
275
+ alter the output style of the command, or parsing will yield incorrect
276
+ results.
277
+
278
+ :return:
279
+ An int defining the number of reachable commits
280
+ """
281
+ # Yes, it makes a difference whether empty paths are given or not in our case as
282
+ # the empty paths version will ignore merge commits for some reason.
283
+ if paths:
284
+ return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines())
285
+ return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines())
286
+
287
+ @property
288
+ def name_rev(self) -> str:
289
+ """
290
+ :return:
291
+ String describing the commits hex sha based on the closest
292
+ :class:`~git.refs.reference.Reference`.
293
+
294
+ :note:
295
+ Mostly useful for UI purposes.
296
+ """
297
+ return self.repo.git.name_rev(self)
298
+
299
+ @classmethod
300
+ def iter_items(
301
+ cls,
302
+ repo: "Repo",
303
+ rev: Union[str, "Commit", "SymbolicReference"],
304
+ paths: Union[PathLike, Sequence[PathLike]] = "",
305
+ **kwargs: Any,
306
+ ) -> Iterator["Commit"]:
307
+ R"""Find all commits matching the given criteria.
308
+
309
+ :param repo:
310
+ The :class:`~git.repo.base.Repo`.
311
+
312
+ :param rev:
313
+ Revision specifier. See :manpage:`git-rev-parse(1)` for viable options.
314
+
315
+ :param paths:
316
+ An optional path or list of paths. If set only :class:`Commit`\s that
317
+ include the path or paths will be considered.
318
+
319
+ :param kwargs:
320
+ Optional keyword arguments to :manpage:`git-rev-list(1)` where:
321
+
322
+ * ``max_count`` is the maximum number of commits to fetch.
323
+ * ``skip`` is the number of commits to skip.
324
+ * ``since`` selects all commits since some date, e.g. ``"1970-01-01"``.
325
+
326
+ :return:
327
+ Iterator yielding :class:`Commit` items.
328
+ """
329
+ if "pretty" in kwargs:
330
+ raise ValueError("--pretty cannot be used as parsing expects single sha's only")
331
+ # END handle pretty
332
+
333
+ # Use -- in all cases, to prevent possibility of ambiguous arguments.
334
+ # See https://github.com/gitpython-developers/GitPython/issues/264.
335
+
336
+ args_list: List[PathLike] = ["--"]
337
+
338
+ if paths:
339
+ paths_tup: Tuple[PathLike, ...]
340
+ if isinstance(paths, (str, os.PathLike)):
341
+ paths_tup = (paths,)
342
+ else:
343
+ paths_tup = tuple(paths)
344
+
345
+ args_list.extend(paths_tup)
346
+ # END if paths
347
+
348
+ proc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs)
349
+ return cls._iter_from_process_or_stream(repo, proc)
350
+
351
+ def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> Iterator["Commit"]:
352
+ R"""Iterate *all* parents of this commit.
353
+
354
+ :param paths:
355
+ Optional path or list of paths limiting the :class:`Commit`\s to those that
356
+ contain at least one of the paths.
357
+
358
+ :param kwargs:
359
+ All arguments allowed by :manpage:`git-rev-list(1)`.
360
+
361
+ :return:
362
+ Iterator yielding :class:`Commit` objects which are parents of ``self``
363
+ """
364
+ # skip ourselves
365
+ skip = kwargs.get("skip", 1)
366
+ if skip == 0: # skip ourselves
367
+ skip = 1
368
+ kwargs["skip"] = skip
369
+
370
+ return self.iter_items(self.repo, self, paths, **kwargs)
371
+
372
+ @property
373
+ def stats(self) -> Stats:
374
+ """Create a git stat from changes between this commit and its first parent
375
+ or from all changes done if this is the very first commit.
376
+
377
+ :return:
378
+ :class:`Stats`
379
+ """
380
+
381
+ def process_lines(lines: List[str]) -> str:
382
+ text = ""
383
+ for file_info, line in zip(lines, lines[len(lines) // 2 :]):
384
+ change_type = file_info.split("\t")[0][-1]
385
+ (insertions, deletions, filename) = line.split("\t")
386
+ text += "%s\t%s\t%s\t%s\n" % (change_type, insertions, deletions, filename)
387
+ return text
388
+
389
+ if not self.parents:
390
+ lines = self.repo.git.diff_tree(
391
+ self.hexsha, "--", numstat=True, no_renames=True, root=True, raw=True
392
+ ).splitlines()[1:]
393
+ text = process_lines(lines)
394
+ else:
395
+ lines = self.repo.git.diff(
396
+ self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True, raw=True
397
+ ).splitlines()
398
+ text = process_lines(lines)
399
+ return Stats._list_from_string(self.repo, text)
400
+
401
+ @property
402
+ def trailers(self) -> Dict[str, str]:
403
+ """Deprecated. Get the trailers of the message as a dictionary.
404
+
405
+ :note:
406
+ This property is deprecated, please use either :attr:`trailers_list` or
407
+ :attr:`trailers_dict`.
408
+
409
+ :return:
410
+ Dictionary containing whitespace stripped trailer information.
411
+ Only contains the latest instance of each trailer key.
412
+ """
413
+ warnings.warn(
414
+ "Commit.trailers is deprecated, use Commit.trailers_list or Commit.trailers_dict instead",
415
+ DeprecationWarning,
416
+ stacklevel=2,
417
+ )
418
+ return {k: v[0] for k, v in self.trailers_dict.items()}
419
+
420
+ @property
421
+ def trailers_list(self) -> List[Tuple[str, str]]:
422
+ """Get the trailers of the message as a list.
423
+
424
+ Git messages can contain trailer information that are similar to :rfc:`822`
425
+ e-mail headers. See :manpage:`git-interpret-trailers(1)`.
426
+
427
+ This function calls ``git interpret-trailers --parse`` onto the message to
428
+ extract the trailer information, returns the raw trailer data as a list.
429
+
430
+ Valid message with trailer::
431
+
432
+ Subject line
433
+
434
+ some body information
435
+
436
+ another information
437
+
438
+ key1: value1.1
439
+ key1: value1.2
440
+ key2 : value 2 with inner spaces
441
+
442
+ Returned list will look like this::
443
+
444
+ [
445
+ ("key1", "value1.1"),
446
+ ("key1", "value1.2"),
447
+ ("key2", "value 2 with inner spaces"),
448
+ ]
449
+
450
+ :return:
451
+ List containing key-value tuples of whitespace stripped trailer information.
452
+ """
453
+ cmd = ["git", "interpret-trailers", "--parse"]
454
+ proc: Git.AutoInterrupt = self.repo.git.execute( # type: ignore[call-overload]
455
+ cmd,
456
+ as_process=True,
457
+ istream=PIPE,
458
+ )
459
+ trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
460
+ trailer = trailer.strip()
461
+
462
+ if not trailer:
463
+ return []
464
+
465
+ trailer_list = []
466
+ for t in trailer.split("\n"):
467
+ key, val = t.split(":", 1)
468
+ trailer_list.append((key.strip(), val.strip()))
469
+
470
+ return trailer_list
471
+
472
+ @property
473
+ def trailers_dict(self) -> Dict[str, List[str]]:
474
+ """Get the trailers of the message as a dictionary.
475
+
476
+ Git messages can contain trailer information that are similar to :rfc:`822`
477
+ e-mail headers. See :manpage:`git-interpret-trailers(1)`.
478
+
479
+ This function calls ``git interpret-trailers --parse`` onto the message to
480
+ extract the trailer information. The key value pairs are stripped of leading and
481
+ trailing whitespaces before they get saved into a dictionary.
482
+
483
+ Valid message with trailer::
484
+
485
+ Subject line
486
+
487
+ some body information
488
+
489
+ another information
490
+
491
+ key1: value1.1
492
+ key1: value1.2
493
+ key2 : value 2 with inner spaces
494
+
495
+ Returned dictionary will look like this::
496
+
497
+ {
498
+ "key1": ["value1.1", "value1.2"],
499
+ "key2": ["value 2 with inner spaces"],
500
+ }
501
+
502
+
503
+ :return:
504
+ Dictionary containing whitespace stripped trailer information, mapping
505
+ trailer keys to a list of their corresponding values.
506
+ """
507
+ d = defaultdict(list)
508
+ for key, val in self.trailers_list:
509
+ d[key].append(val)
510
+ return dict(d)
511
+
512
+ @classmethod
513
+ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:
514
+ """Parse out commit information into a list of :class:`Commit` objects.
515
+
516
+ We expect one line per commit, and parse the actual commit information directly
517
+ from our lighting fast object database.
518
+
519
+ :param proc:
520
+ :manpage:`git-rev-list(1)` process instance - one sha per line.
521
+
522
+ :return:
523
+ Iterator supplying :class:`Commit` objects
524
+ """
525
+
526
+ # def is_proc(inp) -> TypeGuard[Popen]:
527
+ # return hasattr(proc_or_stream, 'wait') and not hasattr(proc_or_stream, 'readline')
528
+
529
+ # def is_stream(inp) -> TypeGuard[IO]:
530
+ # return hasattr(proc_or_stream, 'readline')
531
+
532
+ if hasattr(proc_or_stream, "wait"):
533
+ proc_or_stream = cast(Popen, proc_or_stream)
534
+ if proc_or_stream.stdout is not None:
535
+ stream = proc_or_stream.stdout
536
+ elif hasattr(proc_or_stream, "readline"):
537
+ proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast]
538
+ stream = proc_or_stream
539
+
540
+ readline = stream.readline
541
+ while True:
542
+ line = readline()
543
+ if not line:
544
+ break
545
+ hexsha = line.strip()
546
+ if len(hexsha) > 40:
547
+ # Split additional information, as returned by bisect for instance.
548
+ hexsha, _ = line.split(None, 1)
549
+ # END handle extra info
550
+
551
+ assert len(hexsha) == 40, "Invalid line: %s" % hexsha
552
+ yield cls(repo, hex_to_bin(hexsha))
553
+ # END for each line in stream
554
+
555
+ # TODO: Review this - it seems process handling got a bit out of control due to
556
+ # many developers trying to fix the open file handles issue.
557
+ if hasattr(proc_or_stream, "wait"):
558
+ proc_or_stream = cast(Popen, proc_or_stream)
559
+ finalize_process(proc_or_stream)
560
+
561
+ @classmethod
562
+ def create_from_tree(
563
+ cls,
564
+ repo: "Repo",
565
+ tree: Union[Tree, str],
566
+ message: str,
567
+ parent_commits: Union[None, List["Commit"]] = None,
568
+ head: bool = False,
569
+ author: Union[None, Actor] = None,
570
+ committer: Union[None, Actor] = None,
571
+ author_date: Union[None, str, datetime.datetime] = None,
572
+ commit_date: Union[None, str, datetime.datetime] = None,
573
+ ) -> "Commit":
574
+ """Commit the given tree, creating a :class:`Commit` object.
575
+
576
+ :param repo:
577
+ :class:`~git.repo.base.Repo` object the commit should be part of.
578
+
579
+ :param tree:
580
+ :class:`~git.objects.tree.Tree` object or hex or bin sha.
581
+ The tree of the new commit.
582
+
583
+ :param message:
584
+ Commit message. It may be an empty string if no message is provided. It will
585
+ be converted to a string, in any case.
586
+
587
+ :param parent_commits:
588
+ Optional :class:`Commit` objects to use as parents for the new commit. If
589
+ empty list, the commit will have no parents at all and become a root commit.
590
+ If ``None``, the current head commit will be the parent of the new commit
591
+ object.
592
+
593
+ :param head:
594
+ If ``True``, the HEAD will be advanced to the new commit automatically.
595
+ Otherwise the HEAD will remain pointing on the previous commit. This could
596
+ lead to undesired results when diffing files.
597
+
598
+ :param author:
599
+ The name of the author, optional.
600
+ If unset, the repository configuration is used to obtain this value.
601
+
602
+ :param committer:
603
+ The name of the committer, optional.
604
+ If unset, the repository configuration is used to obtain this value.
605
+
606
+ :param author_date:
607
+ The timestamp for the author field.
608
+
609
+ :param commit_date:
610
+ The timestamp for the committer field.
611
+
612
+ :return:
613
+ :class:`Commit` object representing the new commit.
614
+
615
+ :note:
616
+ Additional information about the committer and author are taken from the
617
+ environment or from the git configuration. See :manpage:`git-commit-tree(1)`
618
+ for more information.
619
+ """
620
+ if parent_commits is None:
621
+ try:
622
+ parent_commits = [repo.head.commit]
623
+ except ValueError:
624
+ # Empty repositories have no head commit.
625
+ parent_commits = []
626
+ # END handle parent commits
627
+ else:
628
+ for p in parent_commits:
629
+ if not isinstance(p, cls):
630
+ raise ValueError(f"Parent commit '{p!r}' must be of type {cls}")
631
+ # END check parent commit types
632
+ # END if parent commits are unset
633
+
634
+ # Retrieve all additional information, create a commit object, and serialize it.
635
+ # Generally:
636
+ # * Environment variables override configuration values.
637
+ # * Sensible defaults are set according to the git documentation.
638
+
639
+ # COMMITTER AND AUTHOR INFO
640
+ cr = repo.config_reader()
641
+ env = os.environ
642
+
643
+ committer = committer or Actor.committer(cr)
644
+ author = author or Actor.author(cr)
645
+
646
+ # PARSE THE DATES
647
+ unix_time = int(time())
648
+ is_dst = daylight and localtime().tm_isdst > 0
649
+ offset = altzone if is_dst else timezone
650
+
651
+ author_date_str = env.get(cls.env_author_date, "")
652
+ if author_date:
653
+ author_time, author_offset = parse_date(author_date)
654
+ elif author_date_str:
655
+ author_time, author_offset = parse_date(author_date_str)
656
+ else:
657
+ author_time, author_offset = unix_time, offset
658
+ # END set author time
659
+
660
+ committer_date_str = env.get(cls.env_committer_date, "")
661
+ if commit_date:
662
+ committer_time, committer_offset = parse_date(commit_date)
663
+ elif committer_date_str:
664
+ committer_time, committer_offset = parse_date(committer_date_str)
665
+ else:
666
+ committer_time, committer_offset = unix_time, offset
667
+ # END set committer time
668
+
669
+ # Assume UTF-8 encoding.
670
+ enc_section, enc_option = cls.conf_encoding.split(".")
671
+ conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding)
672
+ if not isinstance(conf_encoding, str):
673
+ raise TypeError("conf_encoding could not be coerced to str")
674
+
675
+ # If the tree is no object, make sure we create one - otherwise the created
676
+ # commit object is invalid.
677
+ if isinstance(tree, str):
678
+ tree = repo.tree(tree)
679
+ # END tree conversion
680
+
681
+ # CREATE NEW COMMIT
682
+ new_commit = cls(
683
+ repo,
684
+ cls.NULL_BIN_SHA,
685
+ tree,
686
+ author,
687
+ author_time,
688
+ author_offset,
689
+ committer,
690
+ committer_time,
691
+ committer_offset,
692
+ message,
693
+ parent_commits,
694
+ conf_encoding,
695
+ )
696
+
697
+ new_commit.binsha = cls._calculate_sha_(repo, new_commit)
698
+
699
+ if head:
700
+ # Need late import here, importing git at the very beginning throws as
701
+ # well...
702
+ import git.refs
703
+
704
+ try:
705
+ repo.head.set_commit(new_commit, logmsg=message)
706
+ except ValueError:
707
+ # head is not yet set to the ref our HEAD points to.
708
+ # Happens on first commit.
709
+ master = git.refs.Head.create(
710
+ repo,
711
+ repo.head.ref,
712
+ new_commit,
713
+ logmsg="commit (initial): %s" % message,
714
+ )
715
+ repo.head.set_reference(master, logmsg="commit: Switching to %s" % master)
716
+ # END handle empty repositories
717
+ # END advance head handling
718
+
719
+ return new_commit
720
+
721
+ # { Serializable Implementation
722
+
723
+ def _serialize(self, stream: BytesIO) -> "Commit":
724
+ write = stream.write
725
+ write(("tree %s\n" % self.tree).encode("ascii"))
726
+ for p in self.parents:
727
+ write(("parent %s\n" % p).encode("ascii"))
728
+
729
+ a = self.author
730
+ aname = a.name
731
+ c = self.committer
732
+ fmt = "%s %s <%s> %s %s\n"
733
+ write(
734
+ (
735
+ fmt
736
+ % (
737
+ "author",
738
+ aname,
739
+ a.email,
740
+ self.authored_date,
741
+ altz_to_utctz_str(self.author_tz_offset),
742
+ )
743
+ ).encode(self.encoding)
744
+ )
745
+
746
+ # Encode committer.
747
+ aname = c.name
748
+ write(
749
+ (
750
+ fmt
751
+ % (
752
+ "committer",
753
+ aname,
754
+ c.email,
755
+ self.committed_date,
756
+ altz_to_utctz_str(self.committer_tz_offset),
757
+ )
758
+ ).encode(self.encoding)
759
+ )
760
+
761
+ if self.encoding != self.default_encoding:
762
+ write(("encoding %s\n" % self.encoding).encode("ascii"))
763
+
764
+ try:
765
+ if self.__getattribute__("gpgsig"):
766
+ write(b"gpgsig")
767
+ for sigline in self.gpgsig.rstrip("\n").split("\n"):
768
+ write((" " + sigline + "\n").encode("ascii"))
769
+ except AttributeError:
770
+ pass
771
+
772
+ write(b"\n")
773
+
774
+ # Write plain bytes, be sure its encoded according to our encoding.
775
+ if isinstance(self.message, str):
776
+ write(self.message.encode(self.encoding))
777
+ else:
778
+ write(self.message)
779
+ # END handle encoding
780
+ return self
781
+
782
+ def _deserialize(self, stream: BytesIO) -> "Commit":
783
+ readline = stream.readline
784
+ self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "")
785
+
786
+ self.parents = []
787
+ next_line = None
788
+ while True:
789
+ parent_line = readline()
790
+ if not parent_line.startswith(b"parent"):
791
+ next_line = parent_line
792
+ break
793
+ # END abort reading parents
794
+ self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode("ascii"))))
795
+ # END for each parent line
796
+ self.parents = tuple(self.parents)
797
+
798
+ # We don't know actual author encoding before we have parsed it, so keep the
799
+ # lines around.
800
+ author_line = next_line
801
+ committer_line = readline()
802
+
803
+ # We might run into one or more mergetag blocks, skip those for now.
804
+ next_line = readline()
805
+ while next_line.startswith(b"mergetag "):
806
+ next_line = readline()
807
+ while next_line.startswith(b" "):
808
+ next_line = readline()
809
+ # END skip mergetags
810
+
811
+ # Now we can have the encoding line, or an empty line followed by the optional
812
+ # message.
813
+ self.encoding = self.default_encoding
814
+ self.gpgsig = ""
815
+
816
+ # Read headers.
817
+ enc = next_line
818
+ buf = enc.strip()
819
+ while buf:
820
+ if buf[0:10] == b"encoding ":
821
+ self.encoding = buf[buf.find(b" ") + 1 :].decode(self.encoding, "ignore")
822
+ elif buf[0:7] == b"gpgsig ":
823
+ sig = buf[buf.find(b" ") + 1 :] + b"\n"
824
+ is_next_header = False
825
+ while True:
826
+ sigbuf = readline()
827
+ if not sigbuf:
828
+ break
829
+ if sigbuf[0:1] != b" ":
830
+ buf = sigbuf.strip()
831
+ is_next_header = True
832
+ break
833
+ sig += sigbuf[1:]
834
+ # END read all signature
835
+ self.gpgsig = sig.rstrip(b"\n").decode(self.encoding, "ignore")
836
+ if is_next_header:
837
+ continue
838
+ buf = readline().strip()
839
+
840
+ # Decode the author's name.
841
+ try:
842
+ (
843
+ self.author,
844
+ self.authored_date,
845
+ self.author_tz_offset,
846
+ ) = parse_actor_and_date(author_line.decode(self.encoding, "replace"))
847
+ except UnicodeDecodeError:
848
+ _logger.error(
849
+ "Failed to decode author line '%s' using encoding %s",
850
+ author_line,
851
+ self.encoding,
852
+ exc_info=True,
853
+ )
854
+
855
+ try:
856
+ (
857
+ self.committer,
858
+ self.committed_date,
859
+ self.committer_tz_offset,
860
+ ) = parse_actor_and_date(committer_line.decode(self.encoding, "replace"))
861
+ except UnicodeDecodeError:
862
+ _logger.error(
863
+ "Failed to decode committer line '%s' using encoding %s",
864
+ committer_line,
865
+ self.encoding,
866
+ exc_info=True,
867
+ )
868
+ # END handle author's encoding
869
+
870
+ # A stream from our data simply gives us the plain message.
871
+ # The end of our message stream is marked with a newline that we strip.
872
+ self.message = stream.read()
873
+ try:
874
+ self.message = self.message.decode(self.encoding, "replace")
875
+ except UnicodeDecodeError:
876
+ _logger.error(
877
+ "Failed to decode message '%s' using encoding %s",
878
+ self.message,
879
+ self.encoding,
880
+ exc_info=True,
881
+ )
882
+ # END exception handling
883
+
884
+ return self
885
+
886
+ # } END serializable implementation
887
+
888
+ @property
889
+ def co_authors(self) -> List[Actor]:
890
+ """Search the commit message for any co-authors of this commit.
891
+
892
+ Details on co-authors:
893
+ https://github.blog/2018-01-29-commit-together-with-co-authors/
894
+
895
+ :return:
896
+ List of co-authors for this commit (as :class:`~git.util.Actor` objects).
897
+ """
898
+ co_authors = []
899
+
900
+ if self.message:
901
+ results = re.findall(
902
+ r"^Co-authored-by: (.*) <(.*?)>$",
903
+ self.message,
904
+ re.MULTILINE,
905
+ )
906
+ for author in results:
907
+ co_authors.append(Actor(*author))
908
+
909
+ return co_authors
.venv/lib/python3.11/site-packages/git/objects/fun.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Functions that are supposed to be as fast as possible."""
5
+
6
+ __all__ = [
7
+ "tree_to_stream",
8
+ "tree_entries_from_data",
9
+ "traverse_trees_recursive",
10
+ "traverse_tree_recursive",
11
+ ]
12
+
13
+ from stat import S_ISDIR
14
+
15
+ from git.compat import safe_decode, defenc
16
+
17
+ # typing ----------------------------------------------
18
+
19
+ from typing import (
20
+ Callable,
21
+ List,
22
+ MutableSequence,
23
+ Sequence,
24
+ Tuple,
25
+ TYPE_CHECKING,
26
+ Union,
27
+ overload,
28
+ )
29
+
30
+ if TYPE_CHECKING:
31
+ from _typeshed import ReadableBuffer
32
+
33
+ from git import GitCmdObjectDB
34
+
35
+ EntryTup = Tuple[bytes, int, str] # Same as TreeCacheTup in tree.py.
36
+ EntryTupOrNone = Union[EntryTup, None]
37
+
38
+ # ---------------------------------------------------
39
+
40
+
41
+ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
42
+ """Write the given list of entries into a stream using its ``write`` method.
43
+
44
+ :param entries:
45
+ **Sorted** list of tuples with (binsha, mode, name).
46
+
47
+ :param write:
48
+ A ``write`` method which takes a data string.
49
+ """
50
+ ord_zero = ord("0")
51
+ bit_mask = 7 # 3 bits set.
52
+
53
+ for binsha, mode, name in entries:
54
+ mode_str = b""
55
+ for i in range(6):
56
+ mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str
57
+ # END for each 8 octal value
58
+
59
+ # git slices away the first octal if it's zero.
60
+ if mode_str[0] == ord_zero:
61
+ mode_str = mode_str[1:]
62
+ # END save a byte
63
+
64
+ # Here it comes: If the name is actually unicode, the replacement below will not
65
+ # work as the binsha is not part of the ascii unicode encoding - hence we must
66
+ # convert to an UTF-8 string for it to work properly. According to my tests,
67
+ # this is exactly what git does, that is it just takes the input literally,
68
+ # which appears to be UTF-8 on linux.
69
+ if isinstance(name, str):
70
+ name_bytes = name.encode(defenc)
71
+ else:
72
+ name_bytes = name # type: ignore[unreachable] # check runtime types - is always str?
73
+ write(b"".join((mode_str, b" ", name_bytes, b"\0", binsha)))
74
+ # END for each item
75
+
76
+
77
+ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
78
+ """Read the binary representation of a tree and returns tuples of
79
+ :class:`~git.objects.tree.Tree` items.
80
+
81
+ :param data:
82
+ Data block with tree data (as bytes).
83
+
84
+ :return:
85
+ list(tuple(binsha, mode, tree_relative_path), ...)
86
+ """
87
+ ord_zero = ord("0")
88
+ space_ord = ord(" ")
89
+ len_data = len(data)
90
+ i = 0
91
+ out = []
92
+ while i < len_data:
93
+ mode = 0
94
+
95
+ # Read Mode
96
+ # Some git versions truncate the leading 0, some don't.
97
+ # The type will be extracted from the mode later.
98
+ while data[i] != space_ord:
99
+ # Move existing mode integer up one level being 3 bits and add the actual
100
+ # ordinal value of the character.
101
+ mode = (mode << 3) + (data[i] - ord_zero)
102
+ i += 1
103
+ # END while reading mode
104
+
105
+ # Byte is space now, skip it.
106
+ i += 1
107
+
108
+ # Parse name, it is NULL separated.
109
+
110
+ ns = i
111
+ while data[i] != 0:
112
+ i += 1
113
+ # END while not reached NULL
114
+
115
+ # Default encoding for strings in git is UTF-8.
116
+ # Only use the respective unicode object if the byte stream was encoded.
117
+ name_bytes = data[ns:i]
118
+ name = safe_decode(name_bytes)
119
+
120
+ # Byte is NULL, get next 20.
121
+ i += 1
122
+ sha = data[i : i + 20]
123
+ i = i + 20
124
+ out.append((sha, mode, name))
125
+ # END for each byte in data stream
126
+ return out
127
+
128
+
129
+ def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone:
130
+ """Return data entry matching the given name and tree mode or ``None``.
131
+
132
+ Before the item is returned, the respective data item is set None in the `tree_data`
133
+ list to mark it done.
134
+ """
135
+
136
+ try:
137
+ item = tree_data[start_at]
138
+ if item and item[2] == name and S_ISDIR(item[1]) == is_dir:
139
+ tree_data[start_at] = None
140
+ return item
141
+ except IndexError:
142
+ pass
143
+ # END exception handling
144
+ for index, item in enumerate(tree_data):
145
+ if item and item[2] == name and S_ISDIR(item[1]) == is_dir:
146
+ tree_data[index] = None
147
+ return item
148
+ # END if item matches
149
+ # END for each item
150
+ return None
151
+
152
+
153
+ @overload
154
+ def _to_full_path(item: None, path_prefix: str) -> None: ...
155
+
156
+
157
+ @overload
158
+ def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: ...
159
+
160
+
161
+ def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone:
162
+ """Rebuild entry with given path prefix."""
163
+ if not item:
164
+ return item
165
+ return (item[0], item[1], path_prefix + item[2])
166
+
167
+
168
+ def traverse_trees_recursive(
169
+ odb: "GitCmdObjectDB", tree_shas: Sequence[Union[bytes, None]], path_prefix: str
170
+ ) -> List[Tuple[EntryTupOrNone, ...]]:
171
+ """
172
+ :return:
173
+ List of list with entries according to the given binary tree-shas.
174
+
175
+ The result is encoded in a list
176
+ of n tuple|None per blob/commit, (n == len(tree_shas)), where:
177
+
178
+ * [0] == 20 byte sha
179
+ * [1] == mode as int
180
+ * [2] == path relative to working tree root
181
+
182
+ The entry tuple is ``None`` if the respective blob/commit did not exist in the
183
+ given tree.
184
+
185
+ :param tree_shas:
186
+ Iterable of shas pointing to trees. All trees must be on the same level.
187
+ A tree-sha may be ``None``, in which case ``None``.
188
+
189
+ :param path_prefix:
190
+ A prefix to be added to the returned paths on this level.
191
+ Set it ``""`` for the first iteration.
192
+
193
+ :note:
194
+ The ordering of the returned items will be partially lost.
195
+ """
196
+ trees_data: List[List[EntryTupOrNone]] = []
197
+
198
+ nt = len(tree_shas)
199
+ for tree_sha in tree_shas:
200
+ if tree_sha is None:
201
+ data: List[EntryTupOrNone] = []
202
+ else:
203
+ # Make new list for typing as list invariant.
204
+ data = list(tree_entries_from_data(odb.stream(tree_sha).read()))
205
+ # END handle muted trees
206
+ trees_data.append(data)
207
+ # END for each sha to get data for
208
+
209
+ out: List[Tuple[EntryTupOrNone, ...]] = []
210
+
211
+ # Find all matching entries and recursively process them together if the match is a
212
+ # tree. If the match is a non-tree item, put it into the result.
213
+ # Processed items will be set None.
214
+ for ti, tree_data in enumerate(trees_data):
215
+ for ii, item in enumerate(tree_data):
216
+ if not item:
217
+ continue
218
+ # END skip already done items
219
+ entries: List[EntryTupOrNone]
220
+ entries = [None for _ in range(nt)]
221
+ entries[ti] = item
222
+ _sha, mode, name = item
223
+ is_dir = S_ISDIR(mode) # Type mode bits
224
+
225
+ # Find this item in all other tree data items.
226
+ # Wrap around, but stop one before our current index, hence ti+nt, not
227
+ # ti+1+nt.
228
+ for tio in range(ti + 1, ti + nt):
229
+ tio = tio % nt
230
+ entries[tio] = _find_by_name(trees_data[tio], name, is_dir, ii)
231
+
232
+ # END for each other item data
233
+ # If we are a directory, enter recursion.
234
+ if is_dir:
235
+ out.extend(
236
+ traverse_trees_recursive(
237
+ odb,
238
+ [((ei and ei[0]) or None) for ei in entries],
239
+ path_prefix + name + "/",
240
+ )
241
+ )
242
+ else:
243
+ out.append(tuple(_to_full_path(e, path_prefix) for e in entries))
244
+
245
+ # END handle recursion
246
+ # Finally mark it done.
247
+ tree_data[ii] = None
248
+ # END for each item
249
+
250
+ # We are done with one tree, set all its data empty.
251
+ del tree_data[:]
252
+ # END for each tree_data chunk
253
+ return out
254
+
255
+
256
+ def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]:
257
+ """
258
+ :return:
259
+ List of entries of the tree pointed to by the binary `tree_sha`.
260
+
261
+ An entry has the following format:
262
+
263
+ * [0] 20 byte sha
264
+ * [1] mode as int
265
+ * [2] path relative to the repository
266
+
267
+ :param path_prefix:
268
+ Prefix to prepend to the front of all returned paths.
269
+ """
270
+ entries = []
271
+ data = tree_entries_from_data(odb.stream(tree_sha).read())
272
+
273
+ # Unpacking/packing is faster than accessing individual items.
274
+ for sha, mode, name in data:
275
+ if S_ISDIR(mode):
276
+ entries.extend(traverse_tree_recursive(odb, sha, path_prefix + name + "/"))
277
+ else:
278
+ entries.append((sha, mode, path_prefix + name))
279
+ # END for each item
280
+
281
+ return entries
.venv/lib/python3.11/site-packages/git/objects/submodule/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["Submodule", "UpdateProgress", "RootModule", "RootUpdateProgress"]
5
+
6
+ from .base import Submodule, UpdateProgress
7
+ from .root import RootModule, RootUpdateProgress
.venv/lib/python3.11/site-packages/git/objects/submodule/base.py ADDED
@@ -0,0 +1,1642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["Submodule", "UpdateProgress"]
5
+
6
+ import gc
7
+ from io import BytesIO
8
+ import logging
9
+ import os
10
+ import os.path as osp
11
+ import stat
12
+ import sys
13
+ import uuid
14
+ import urllib
15
+
16
+ import git
17
+ from git.cmd import Git
18
+ from git.compat import defenc
19
+ from git.config import GitConfigParser, SectionConstraint, cp
20
+ from git.exc import (
21
+ BadName,
22
+ InvalidGitRepositoryError,
23
+ NoSuchPathError,
24
+ RepositoryDirtyError,
25
+ )
26
+ from git.objects.base import IndexObject, Object
27
+ from git.objects.util import TraversableIterableObj
28
+ from git.util import (
29
+ IterableList,
30
+ RemoteProgress,
31
+ join_path_native,
32
+ rmtree,
33
+ to_native_path_linux,
34
+ unbare_repo,
35
+ )
36
+
37
+ from .util import (
38
+ SubmoduleConfigParser,
39
+ find_first_remote_branch,
40
+ mkhead,
41
+ sm_name,
42
+ sm_section,
43
+ )
44
+
45
+ # typing ----------------------------------------------------------------------
46
+
47
+ from typing import (
48
+ Any,
49
+ Callable,
50
+ Dict,
51
+ Iterator,
52
+ Mapping,
53
+ Sequence,
54
+ TYPE_CHECKING,
55
+ Union,
56
+ cast,
57
+ )
58
+
59
+ if sys.version_info >= (3, 8):
60
+ from typing import Literal
61
+ else:
62
+ from typing_extensions import Literal
63
+
64
+ from git.types import Commit_ish, PathLike, TBD
65
+
66
+ if TYPE_CHECKING:
67
+ from git.index import IndexFile
68
+ from git.objects.commit import Commit
69
+ from git.refs import Head
70
+ from git.repo import Repo
71
+
72
+ # -----------------------------------------------------------------------------
73
+
74
+ _logger = logging.getLogger(__name__)
75
+
76
+
77
+ class UpdateProgress(RemoteProgress):
78
+ """Class providing detailed progress information to the caller who should
79
+ derive from it and implement the
80
+ :meth:`update(...) <git.util.RemoteProgress.update>` message."""
81
+
82
+ CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
83
+ _num_op_codes: int = RemoteProgress._num_op_codes + 3
84
+
85
+ __slots__ = ()
86
+
87
+
88
+ BEGIN = UpdateProgress.BEGIN
89
+ END = UpdateProgress.END
90
+ CLONE = UpdateProgress.CLONE
91
+ FETCH = UpdateProgress.FETCH
92
+ UPDWKTREE = UpdateProgress.UPDWKTREE
93
+
94
+
95
+ # IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
96
+ # mechanism, which causes plenty of trouble if the only reason for packages and modules
97
+ # is refactoring - subpackages shouldn't depend on parent packages.
98
+ class Submodule(IndexObject, TraversableIterableObj):
99
+ """Implements access to a git submodule. They are special in that their sha
100
+ represents a commit in the submodule's repository which is to be checked out
101
+ at the path of this instance.
102
+
103
+ The submodule type does not have a string type associated with it, as it exists
104
+ solely as a marker in the tree and index.
105
+
106
+ All methods work in bare and non-bare repositories.
107
+ """
108
+
109
+ _id_attribute_ = "name"
110
+ k_modules_file = ".gitmodules"
111
+ k_head_option = "branch"
112
+ k_head_default = "master"
113
+ k_default_mode = stat.S_IFDIR | stat.S_IFLNK
114
+ """Submodule flags. Submodules are directories with link-status."""
115
+
116
+ type: Literal["submodule"] = "submodule" # type: ignore[assignment]
117
+ """This is a bogus type string for base class compatibility."""
118
+
119
+ __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
120
+
121
+ _cache_attrs = ("path", "_url", "_branch_path")
122
+
123
+ def __init__(
124
+ self,
125
+ repo: "Repo",
126
+ binsha: bytes,
127
+ mode: Union[int, None] = None,
128
+ path: Union[PathLike, None] = None,
129
+ name: Union[str, None] = None,
130
+ parent_commit: Union["Commit", None] = None,
131
+ url: Union[str, None] = None,
132
+ branch_path: Union[PathLike, None] = None,
133
+ ) -> None:
134
+ """Initialize this instance with its attributes.
135
+
136
+ We only document the parameters that differ from
137
+ :class:`~git.objects.base.IndexObject`.
138
+
139
+ :param repo:
140
+ Our parent repository.
141
+
142
+ :param binsha:
143
+ Binary sha referring to a commit in the remote repository.
144
+ See the `url` parameter.
145
+
146
+ :param parent_commit:
147
+ The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
148
+ the ``.gitmodules`` blob, or ``None`` to always point to the most recent
149
+ commit. See :meth:`set_parent_commit` for details.
150
+
151
+ :param url:
152
+ The URL to the remote repository which is the submodule.
153
+
154
+ :param branch_path:
155
+ Full repository-relative path to ref to checkout when cloning the remote
156
+ repository.
157
+ """
158
+ super().__init__(repo, binsha, mode, path)
159
+ self.size = 0
160
+ self._parent_commit = parent_commit
161
+ if url is not None:
162
+ self._url = url
163
+ if branch_path is not None:
164
+ self._branch_path = branch_path
165
+ if name is not None:
166
+ self._name = name
167
+
168
+ def _set_cache_(self, attr: str) -> None:
169
+ if attr in ("path", "_url", "_branch_path"):
170
+ reader: SectionConstraint = self.config_reader()
171
+ # Default submodule values.
172
+ try:
173
+ self.path = reader.get("path")
174
+ except cp.NoSectionError as e:
175
+ if self.repo.working_tree_dir is not None:
176
+ raise ValueError(
177
+ "This submodule instance does not exist anymore in '%s' file"
178
+ % osp.join(self.repo.working_tree_dir, ".gitmodules")
179
+ ) from e
180
+
181
+ self._url = reader.get("url")
182
+ # GitPython extension values - optional.
183
+ self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
184
+ elif attr == "_name":
185
+ raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
186
+ else:
187
+ super()._set_cache_(attr)
188
+ # END handle attribute name
189
+
190
+ @classmethod
191
+ def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]:
192
+ """:return: All the submodules of our module repository"""
193
+ try:
194
+ return cls.list_items(item.module())
195
+ except InvalidGitRepositoryError:
196
+ return IterableList("")
197
+ # END handle intermediate items
198
+
199
+ @classmethod
200
+ def _need_gitfile_submodules(cls, git: Git) -> bool:
201
+ return git.version_info[:3] >= (1, 7, 5)
202
+
203
+ def __eq__(self, other: Any) -> bool:
204
+ """Compare with another submodule."""
205
+ # We may only compare by name as this should be the ID they are hashed with.
206
+ # Otherwise this type wouldn't be hashable.
207
+ # return self.path == other.path and self.url == other.url and super().__eq__(other)
208
+ return self._name == other._name
209
+
210
+ def __ne__(self, other: object) -> bool:
211
+ """Compare with another submodule for inequality."""
212
+ return not (self == other)
213
+
214
+ def __hash__(self) -> int:
215
+ """Hash this instance using its logical id, not the sha."""
216
+ return hash(self._name)
217
+
218
+ def __str__(self) -> str:
219
+ return self._name
220
+
221
+ def __repr__(self) -> str:
222
+ return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % (
223
+ type(self).__name__,
224
+ self._name,
225
+ self.path,
226
+ self.url,
227
+ self.branch_path,
228
+ )
229
+
230
+ @classmethod
231
+ def _config_parser(
232
+ cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
233
+ ) -> SubmoduleConfigParser:
234
+ """
235
+ :return:
236
+ Config parser constrained to our submodule in read or write mode
237
+
238
+ :raise IOError:
239
+ If the ``.gitmodules`` file cannot be found, either locally or in the
240
+ repository at the given parent commit. Otherwise the exception would be
241
+ delayed until the first access of the config parser.
242
+ """
243
+ parent_matches_head = True
244
+ if parent_commit is not None:
245
+ try:
246
+ parent_matches_head = repo.head.commit == parent_commit
247
+ except ValueError:
248
+ # We are most likely in an empty repository, so the HEAD doesn't point
249
+ # to a valid ref.
250
+ pass
251
+ # END handle parent_commit
252
+ fp_module: Union[str, BytesIO]
253
+ if not repo.bare and parent_matches_head and repo.working_tree_dir:
254
+ fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
255
+ else:
256
+ assert parent_commit is not None, "need valid parent_commit in bare repositories"
257
+ try:
258
+ fp_module = cls._sio_modules(parent_commit)
259
+ except KeyError as e:
260
+ raise IOError(
261
+ "Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit)
262
+ ) from e
263
+ # END handle exceptions
264
+ # END handle non-bare working tree
265
+
266
+ if not read_only and (repo.bare or not parent_matches_head):
267
+ raise ValueError("Cannot write blobs of 'historical' submodule configurations")
268
+ # END handle writes of historical submodules
269
+
270
+ return SubmoduleConfigParser(fp_module, read_only=read_only)
271
+
272
+ def _clear_cache(self) -> None:
273
+ """Clear the possibly changed values."""
274
+ for name in self._cache_attrs:
275
+ try:
276
+ delattr(self, name)
277
+ except AttributeError:
278
+ pass
279
+ # END try attr deletion
280
+ # END for each name to delete
281
+
282
+ @classmethod
283
+ def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
284
+ """
285
+ :return:
286
+ Configuration file as :class:`~io.BytesIO` - we only access it through the
287
+ respective blob's data
288
+ """
289
+ sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
290
+ sio.name = cls.k_modules_file
291
+ return sio
292
+
293
+ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
294
+ """:return: Config parser constrained to our submodule in read or write mode"""
295
+ try:
296
+ pc = self.parent_commit
297
+ except ValueError:
298
+ pc = None
299
+ # END handle empty parent repository
300
+ parser = self._config_parser(self.repo, pc, read_only)
301
+ parser.set_submodule(self)
302
+ return SectionConstraint(parser, sm_section(self.name))
303
+
304
+ @classmethod
305
+ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
306
+ if cls._need_gitfile_submodules(parent_repo.git):
307
+ return osp.join(parent_repo.git_dir, "modules", name)
308
+ if parent_repo.working_tree_dir:
309
+ return osp.join(parent_repo.working_tree_dir, path)
310
+ raise NotADirectoryError()
311
+
312
+ @classmethod
313
+ def _clone_repo(
314
+ cls,
315
+ repo: "Repo",
316
+ url: str,
317
+ path: PathLike,
318
+ name: str,
319
+ allow_unsafe_options: bool = False,
320
+ allow_unsafe_protocols: bool = False,
321
+ **kwargs: Any,
322
+ ) -> "Repo":
323
+ """
324
+ :return:
325
+ :class:`~git.repo.base.Repo` instance of newly cloned repository.
326
+
327
+ :param repo:
328
+ Our parent repository.
329
+
330
+ :param url:
331
+ URL to clone from.
332
+
333
+ :param path:
334
+ Repository-relative path to the submodule checkout location.
335
+
336
+ :param name:
337
+ Canonical name of the submodule.
338
+
339
+ :param allow_unsafe_protocols:
340
+ Allow unsafe protocols to be used, like ``ext``.
341
+
342
+ :param allow_unsafe_options:
343
+ Allow unsafe options to be used, like ``--upload-pack``.
344
+
345
+ :param kwargs:
346
+ Additional arguments given to :manpage:`git-clone(1)`.
347
+ """
348
+ module_abspath = cls._module_abspath(repo, path, name)
349
+ module_checkout_path = module_abspath
350
+ if cls._need_gitfile_submodules(repo.git):
351
+ kwargs["separate_git_dir"] = module_abspath
352
+ module_abspath_dir = osp.dirname(module_abspath)
353
+ if not osp.isdir(module_abspath_dir):
354
+ os.makedirs(module_abspath_dir)
355
+ module_checkout_path = osp.join(str(repo.working_tree_dir), path)
356
+
357
+ if url.startswith("../"):
358
+ remote_name = repo.active_branch.tracking_branch().remote_name
359
+ repo_remote_url = repo.remote(remote_name).url
360
+ url = os.path.join(repo_remote_url, url)
361
+
362
+ clone = git.Repo.clone_from(
363
+ url,
364
+ module_checkout_path,
365
+ allow_unsafe_options=allow_unsafe_options,
366
+ allow_unsafe_protocols=allow_unsafe_protocols,
367
+ **kwargs,
368
+ )
369
+ if cls._need_gitfile_submodules(repo.git):
370
+ cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
371
+
372
+ return clone
373
+
374
+ @classmethod
375
+ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
376
+ """:return: A path guaranteed to be relative to the given parent repository
377
+
378
+ :raise ValueError:
379
+ If path is not contained in the parent repository's working tree.
380
+ """
381
+ path = to_native_path_linux(path)
382
+ if path.endswith("/"):
383
+ path = path[:-1]
384
+ # END handle trailing slash
385
+
386
+ if osp.isabs(path) and parent_repo.working_tree_dir:
387
+ working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
388
+ if not path.startswith(working_tree_linux):
389
+ raise ValueError(
390
+ "Submodule checkout path '%s' needs to be within the parents repository at '%s'"
391
+ % (working_tree_linux, path)
392
+ )
393
+ path = path[len(working_tree_linux.rstrip("/")) + 1 :]
394
+ if not path:
395
+ raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
396
+ # END verify converted relative path makes sense
397
+ # END convert to a relative path
398
+
399
+ return path
400
+
401
+ @classmethod
402
+ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
403
+ """Write a ``.git`` file containing a (preferably) relative path to the actual
404
+ git module repository.
405
+
406
+ It is an error if the `module_abspath` cannot be made into a relative path,
407
+ relative to the `working_tree_dir`.
408
+
409
+ :note:
410
+ This will overwrite existing files!
411
+
412
+ :note:
413
+ As we rewrite both the git file as well as the module configuration, we
414
+ might fail on the configuration and will not roll back changes done to the
415
+ git file. This should be a non-issue, but may easily be fixed if it becomes
416
+ one.
417
+
418
+ :param working_tree_dir:
419
+ Directory to write the ``.git`` file into.
420
+
421
+ :param module_abspath:
422
+ Absolute path to the bare repository.
423
+ """
424
+ git_file = osp.join(working_tree_dir, ".git")
425
+ rela_path = osp.relpath(module_abspath, start=working_tree_dir)
426
+ if sys.platform == "win32" and osp.isfile(git_file):
427
+ os.remove(git_file)
428
+ with open(git_file, "wb") as fp:
429
+ fp.write(("gitdir: %s" % rela_path).encode(defenc))
430
+
431
+ with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer:
432
+ writer.set_value(
433
+ "core",
434
+ "worktree",
435
+ to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)),
436
+ )
437
+
438
+ # { Edit Interface
439
+
440
+ @classmethod
441
+ def add(
442
+ cls,
443
+ repo: "Repo",
444
+ name: str,
445
+ path: PathLike,
446
+ url: Union[str, None] = None,
447
+ branch: Union[str, None] = None,
448
+ no_checkout: bool = False,
449
+ depth: Union[int, None] = None,
450
+ env: Union[Mapping[str, str], None] = None,
451
+ clone_multi_options: Union[Sequence[TBD], None] = None,
452
+ allow_unsafe_options: bool = False,
453
+ allow_unsafe_protocols: bool = False,
454
+ ) -> "Submodule":
455
+ """Add a new submodule to the given repository. This will alter the index as
456
+ well as the ``.gitmodules`` file, but will not create a new commit. If the
457
+ submodule already exists, no matter if the configuration differs from the one
458
+ provided, the existing submodule will be returned.
459
+
460
+ :param repo:
461
+ Repository instance which should receive the submodule.
462
+
463
+ :param name:
464
+ The name/identifier for the submodule.
465
+
466
+ :param path:
467
+ Repository-relative or absolute path at which the submodule should be
468
+ located.
469
+ It will be created as required during the repository initialization.
470
+
471
+ :param url:
472
+ ``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more
473
+ information. If ``None``, the repository is assumed to exist, and the URL of
474
+ the first remote is taken instead. This is useful if you want to make an
475
+ existing repository a submodule of another one.
476
+
477
+ :param branch:
478
+ Name of branch at which the submodule should (later) be checked out. The
479
+ given branch must exist in the remote repository, and will be checked out
480
+ locally as a tracking branch.
481
+ It will only be written into the configuration if it not ``None``, which is
482
+ when the checked out branch will be the one the remote HEAD pointed to.
483
+ The result you get in these situation is somewhat fuzzy, and it is
484
+ recommended to specify at least ``master`` here.
485
+ Examples are ``master`` or ``feature/new``.
486
+
487
+ :param no_checkout:
488
+ If ``True``, and if the repository has to be cloned manually, no checkout
489
+ will be performed.
490
+
491
+ :param depth:
492
+ Create a shallow clone with a history truncated to the specified number of
493
+ commits.
494
+
495
+ :param env:
496
+ Optional dictionary containing the desired environment variables.
497
+
498
+ Note: Provided variables will be used to update the execution environment
499
+ for ``git``. If some variable is not specified in `env` and is defined in
500
+ attr:`os.environ`, the value from attr:`os.environ` will be used. If you
501
+ want to unset some variable, consider providing an empty string as its
502
+ value.
503
+
504
+ :param clone_multi_options:
505
+ A list of clone options. Please see
506
+ :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
507
+
508
+ :param allow_unsafe_protocols:
509
+ Allow unsafe protocols to be used, like ``ext``.
510
+
511
+ :param allow_unsafe_options:
512
+ Allow unsafe options to be used, like ``--upload-pack``.
513
+
514
+ :return:
515
+ The newly created :class:`Submodule` instance.
516
+
517
+ :note:
518
+ Works atomically, such that no change will be done if, for example, the
519
+ repository update fails.
520
+ """
521
+ if repo.bare:
522
+ raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
523
+ # END handle bare repos
524
+
525
+ path = cls._to_relative_path(repo, path)
526
+
527
+ # Ensure we never put backslashes into the URL, as might happen on Windows.
528
+ if url is not None:
529
+ url = to_native_path_linux(url)
530
+ # END ensure URL correctness
531
+
532
+ # INSTANTIATE INTERMEDIATE SM
533
+ sm = cls(
534
+ repo,
535
+ cls.NULL_BIN_SHA,
536
+ cls.k_default_mode,
537
+ path,
538
+ name,
539
+ url="invalid-temporary",
540
+ )
541
+ if sm.exists():
542
+ # Reretrieve submodule from tree.
543
+ try:
544
+ sm = repo.head.commit.tree[str(path)]
545
+ sm._name = name
546
+ return sm
547
+ except KeyError:
548
+ # Could only be in index.
549
+ index = repo.index
550
+ entry = index.entries[index.entry_key(path, 0)]
551
+ sm.binsha = entry.binsha
552
+ return sm
553
+ # END handle exceptions
554
+ # END handle existing
555
+
556
+ # fake-repo - we only need the functionality on the branch instance.
557
+ br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
558
+ has_module = sm.module_exists()
559
+ branch_is_default = branch is None
560
+ if has_module and url is not None:
561
+ if url not in [r.url for r in sm.module().remotes]:
562
+ raise ValueError(
563
+ "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath)
564
+ )
565
+ # END check url
566
+ # END verify urls match
567
+
568
+ mrepo: Union[Repo, None] = None
569
+
570
+ if url is None:
571
+ if not has_module:
572
+ raise ValueError("A URL was not given and a repository did not exist at %s" % path)
573
+ # END check url
574
+ mrepo = sm.module()
575
+ # assert isinstance(mrepo, git.Repo)
576
+ urls = [r.url for r in mrepo.remotes]
577
+ if not urls:
578
+ raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath)
579
+ # END verify we have url
580
+ url = urls[0]
581
+ else:
582
+ # Clone new repo.
583
+ kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout}
584
+ if not branch_is_default:
585
+ kwargs["b"] = br.name
586
+ # END setup checkout-branch
587
+
588
+ if depth:
589
+ if isinstance(depth, int):
590
+ kwargs["depth"] = depth
591
+ else:
592
+ raise ValueError("depth should be an integer")
593
+ if clone_multi_options:
594
+ kwargs["multi_options"] = clone_multi_options
595
+
596
+ # _clone_repo(cls, repo, url, path, name, **kwargs):
597
+ mrepo = cls._clone_repo(
598
+ repo,
599
+ url,
600
+ path,
601
+ name,
602
+ env=env,
603
+ allow_unsafe_options=allow_unsafe_options,
604
+ allow_unsafe_protocols=allow_unsafe_protocols,
605
+ **kwargs,
606
+ )
607
+ # END verify url
608
+
609
+ ## See #525 for ensuring git URLs in config-files are valid under Windows.
610
+ url = Git.polish_url(url)
611
+
612
+ # It's important to add the URL to the parent config, to let `git submodule` know.
613
+ # Otherwise there is a '-' character in front of the submodule listing:
614
+ # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
615
+ # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
616
+ writer: Union[GitConfigParser, SectionConstraint]
617
+
618
+ with sm.repo.config_writer() as writer:
619
+ writer.set_value(sm_section(name), "url", url)
620
+
621
+ # Update configuration and index.
622
+ index = sm.repo.index
623
+ with sm.config_writer(index=index, write=False) as writer:
624
+ writer.set_value("url", url)
625
+ writer.set_value("path", path)
626
+
627
+ sm._url = url
628
+ if not branch_is_default:
629
+ # Store full path.
630
+ writer.set_value(cls.k_head_option, br.path)
631
+ sm._branch_path = br.path
632
+
633
+ # We deliberately assume that our head matches our index!
634
+ if mrepo:
635
+ sm.binsha = mrepo.head.commit.binsha
636
+ index.add([sm], write=True)
637
+
638
+ return sm
639
+
640
+ def update(
641
+ self,
642
+ recursive: bool = False,
643
+ init: bool = True,
644
+ to_latest_revision: bool = False,
645
+ progress: Union["UpdateProgress", None] = None,
646
+ dry_run: bool = False,
647
+ force: bool = False,
648
+ keep_going: bool = False,
649
+ env: Union[Mapping[str, str], None] = None,
650
+ clone_multi_options: Union[Sequence[TBD], None] = None,
651
+ allow_unsafe_options: bool = False,
652
+ allow_unsafe_protocols: bool = False,
653
+ ) -> "Submodule":
654
+ """Update the repository of this submodule to point to the checkout we point at
655
+ with the binsha of this instance.
656
+
657
+ :param recursive:
658
+ If ``True``, we will operate recursively and update child modules as well.
659
+
660
+ :param init:
661
+ If ``True``, the module repository will be cloned into place if necessary.
662
+
663
+ :param to_latest_revision:
664
+ If ``True``, the submodule's sha will be ignored during checkout. Instead,
665
+ the remote will be fetched, and the local tracking branch updated. This only
666
+ works if we have a local tracking branch, which is the case if the remote
667
+ repository had a master branch, or if the ``branch`` option was specified
668
+ for this submodule and the branch existed remotely.
669
+
670
+ :param progress:
671
+ :class:`UpdateProgress` instance, or ``None`` if no progress should be
672
+ shown.
673
+
674
+ :param dry_run:
675
+ If ``True``, the operation will only be simulated, but not performed.
676
+ All performed operations are read-only.
677
+
678
+ :param force:
679
+ If ``True``, we may reset heads even if the repository in question is dirty.
680
+ Additionally we will be allowed to set a tracking branch which is ahead of
681
+ its remote branch back into the past or the location of the remote branch.
682
+ This will essentially 'forget' commits.
683
+
684
+ If ``False``, local tracking branches that are in the future of their
685
+ respective remote branches will simply not be moved.
686
+
687
+ :param keep_going:
688
+ If ``True``, we will ignore but log all errors, and keep going recursively.
689
+ Unless `dry_run` is set as well, `keep_going` could cause
690
+ subsequent/inherited errors you wouldn't see otherwise.
691
+ In conjunction with `dry_run`, it can be useful to anticipate all errors
692
+ when updating submodules.
693
+
694
+ :param env:
695
+ Optional dictionary containing the desired environment variables.
696
+
697
+ Note: Provided variables will be used to update the execution environment
698
+ for ``git``. If some variable is not specified in `env` and is defined in
699
+ attr:`os.environ`, value from attr:`os.environ` will be used.
700
+
701
+ If you want to unset some variable, consider providing the empty string as
702
+ its value.
703
+
704
+ :param clone_multi_options:
705
+ List of :manpage:`git-clone(1)` options.
706
+ Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
707
+ They only take effect with the `init` option.
708
+
709
+ :param allow_unsafe_protocols:
710
+ Allow unsafe protocols to be used, like ``ext``.
711
+
712
+ :param allow_unsafe_options:
713
+ Allow unsafe options to be used, like ``--upload-pack``.
714
+
715
+ :note:
716
+ Does nothing in bare repositories.
717
+
718
+ :note:
719
+ This method is definitely not atomic if `recursive` is ``True``.
720
+
721
+ :return:
722
+ self
723
+ """
724
+ if self.repo.bare:
725
+ return self
726
+ # END pass in bare mode
727
+
728
+ if progress is None:
729
+ progress = UpdateProgress()
730
+ # END handle progress
731
+ prefix = ""
732
+ if dry_run:
733
+ prefix = "DRY-RUN: "
734
+ # END handle prefix
735
+
736
+ # To keep things plausible in dry-run mode.
737
+ if dry_run:
738
+ mrepo = None
739
+ # END init mrepo
740
+
741
+ try:
742
+ # ENSURE REPO IS PRESENT AND UP-TO-DATE
743
+ #######################################
744
+ try:
745
+ mrepo = self.module()
746
+ rmts = mrepo.remotes
747
+ len_rmts = len(rmts)
748
+ for i, remote in enumerate(rmts):
749
+ op = FETCH
750
+ if i == 0:
751
+ op |= BEGIN
752
+ # END handle start
753
+
754
+ progress.update(
755
+ op,
756
+ i,
757
+ len_rmts,
758
+ prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
759
+ )
760
+ # ===============================
761
+ if not dry_run:
762
+ remote.fetch(progress=progress)
763
+ # END handle dry-run
764
+ # ===============================
765
+ if i == len_rmts - 1:
766
+ op |= END
767
+ # END handle end
768
+ progress.update(
769
+ op,
770
+ i,
771
+ len_rmts,
772
+ prefix + "Done fetching remote of submodule %r" % self.name,
773
+ )
774
+ # END fetch new data
775
+ except InvalidGitRepositoryError:
776
+ mrepo = None
777
+ if not init:
778
+ return self
779
+ # END early abort if init is not allowed
780
+
781
+ # There is no git-repository yet - but delete empty paths.
782
+ checkout_module_abspath = self.abspath
783
+ if not dry_run and osp.isdir(checkout_module_abspath):
784
+ try:
785
+ os.rmdir(checkout_module_abspath)
786
+ except OSError as e:
787
+ raise OSError(
788
+ "Module directory at %r does already exist and is non-empty" % checkout_module_abspath
789
+ ) from e
790
+ # END handle OSError
791
+ # END handle directory removal
792
+
793
+ # Don't check it out at first - nonetheless it will create a local
794
+ # branch according to the remote-HEAD if possible.
795
+ progress.update(
796
+ BEGIN | CLONE,
797
+ 0,
798
+ 1,
799
+ prefix
800
+ + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
801
+ )
802
+ if not dry_run:
803
+ if self.url.startswith("."):
804
+ url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
805
+ else:
806
+ url = self.url
807
+ mrepo = self._clone_repo(
808
+ self.repo,
809
+ url,
810
+ self.path,
811
+ self.name,
812
+ n=True,
813
+ env=env,
814
+ multi_options=clone_multi_options,
815
+ allow_unsafe_options=allow_unsafe_options,
816
+ allow_unsafe_protocols=allow_unsafe_protocols,
817
+ )
818
+ # END handle dry-run
819
+ progress.update(
820
+ END | CLONE,
821
+ 0,
822
+ 1,
823
+ prefix + "Done cloning to %s" % checkout_module_abspath,
824
+ )
825
+
826
+ if not dry_run:
827
+ # See whether we have a valid branch to check out.
828
+ try:
829
+ mrepo = cast("Repo", mrepo)
830
+ # Find a remote which has our branch - we try to be flexible.
831
+ remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
832
+ local_branch = mkhead(mrepo, self.branch_path)
833
+
834
+ # Have a valid branch, but no checkout - make sure we can figure
835
+ # that out by marking the commit with a null_sha.
836
+ local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
837
+ # END initial checkout + branch creation
838
+
839
+ # Make sure HEAD is not detached.
840
+ mrepo.head.set_reference(
841
+ local_branch,
842
+ logmsg="submodule: attaching head to %s" % local_branch,
843
+ )
844
+ mrepo.head.reference.set_tracking_branch(remote_branch)
845
+ except (IndexError, InvalidGitRepositoryError):
846
+ _logger.warning("Failed to checkout tracking branch %s", self.branch_path)
847
+ # END handle tracking branch
848
+
849
+ # NOTE: Have to write the repo config file as well, otherwise the
850
+ # default implementation will be offended and not update the
851
+ # repository. Maybe this is a good way to ensure it doesn't get into
852
+ # our way, but we want to stay backwards compatible too... It's so
853
+ # redundant!
854
+ with self.repo.config_writer() as writer:
855
+ writer.set_value(sm_section(self.name), "url", self.url)
856
+ # END handle dry_run
857
+ # END handle initialization
858
+
859
+ # DETERMINE SHAS TO CHECK OUT
860
+ #############################
861
+ binsha = self.binsha
862
+ hexsha = self.hexsha
863
+ if mrepo is not None:
864
+ # mrepo is only set if we are not in dry-run mode or if the module
865
+ # existed.
866
+ is_detached = mrepo.head.is_detached
867
+ # END handle dry_run
868
+
869
+ if mrepo is not None and to_latest_revision:
870
+ msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir
871
+ if not is_detached:
872
+ rref = mrepo.head.reference.tracking_branch()
873
+ if rref is not None:
874
+ rcommit = rref.commit
875
+ binsha = rcommit.binsha
876
+ hexsha = rcommit.hexsha
877
+ else:
878
+ _logger.error(
879
+ "%s a tracking branch was not set for local branch '%s'",
880
+ msg_base,
881
+ mrepo.head.reference,
882
+ )
883
+ # END handle remote ref
884
+ else:
885
+ _logger.error("%s there was no local tracking branch", msg_base)
886
+ # END handle detached head
887
+ # END handle to_latest_revision option
888
+
889
+ # Update the working tree.
890
+ # Handles dry_run.
891
+ if mrepo is not None and mrepo.head.commit.binsha != binsha:
892
+ # We must ensure that our destination sha (the one to point to) is in
893
+ # the future of our current head. Otherwise, we will reset changes that
894
+ # might have been done on the submodule, but were not yet pushed. We
895
+ # also handle the case that history has been rewritten, leaving no
896
+ # merge-base. In that case we behave conservatively, protecting possible
897
+ # changes the user had done.
898
+ may_reset = True
899
+ if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
900
+ base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
901
+ if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha):
902
+ if force:
903
+ msg = "Will force checkout or reset on local branch that is possibly in the future of"
904
+ msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
905
+ _logger.debug(msg)
906
+ else:
907
+ msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
908
+ msg %= (
909
+ is_detached and "checkout" or "reset",
910
+ mrepo.head,
911
+ mrepo,
912
+ )
913
+ _logger.info(msg)
914
+ may_reset = False
915
+ # END handle force
916
+ # END handle if we are in the future
917
+
918
+ if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
919
+ raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
920
+ # END handle force and dirty state
921
+ # END handle empty repo
922
+
923
+ # END verify future/past
924
+ progress.update(
925
+ BEGIN | UPDWKTREE,
926
+ 0,
927
+ 1,
928
+ prefix
929
+ + "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha),
930
+ )
931
+
932
+ if not dry_run and may_reset:
933
+ if is_detached:
934
+ # NOTE: For now we force. The user is not supposed to change
935
+ # detached submodules anyway. Maybe at some point this becomes
936
+ # an option, to properly handle user modifications - see below
937
+ # for future options regarding rebase and merge.
938
+ mrepo.git.checkout(hexsha, force=force)
939
+ else:
940
+ mrepo.head.reset(hexsha, index=True, working_tree=True)
941
+ # END handle checkout
942
+ # If we may reset/checkout.
943
+ progress.update(
944
+ END | UPDWKTREE,
945
+ 0,
946
+ 1,
947
+ prefix + "Done updating working tree for submodule %r" % self.name,
948
+ )
949
+ # END update to new commit only if needed
950
+ except Exception as err:
951
+ if not keep_going:
952
+ raise
953
+ _logger.error(str(err))
954
+ # END handle keep_going
955
+
956
+ # HANDLE RECURSION
957
+ ##################
958
+ if recursive:
959
+ # In dry_run mode, the module might not exist.
960
+ if mrepo is not None:
961
+ for submodule in self.iter_items(self.module()):
962
+ submodule.update(
963
+ recursive,
964
+ init,
965
+ to_latest_revision,
966
+ progress=progress,
967
+ dry_run=dry_run,
968
+ force=force,
969
+ keep_going=keep_going,
970
+ )
971
+ # END handle recursive update
972
+ # END handle dry run
973
+ # END for each submodule
974
+
975
+ return self
976
+
977
+ @unbare_repo
978
+ def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule":
979
+ """Move the submodule to a another module path. This involves physically moving
980
+ the repository at our current path, changing the configuration, as well as
981
+ adjusting our index entry accordingly.
982
+
983
+ :param module_path:
984
+ The path to which to move our module in the parent repository's working
985
+ tree, given as repository-relative or absolute path. Intermediate
986
+ directories will be created accordingly. If the path already exists, it must
987
+ be empty. Trailing (back)slashes are removed automatically.
988
+
989
+ :param configuration:
990
+ If ``True``, the configuration will be adjusted to let the submodule point
991
+ to the given path.
992
+
993
+ :param module:
994
+ If ``True``, the repository managed by this submodule will be moved as well.
995
+ If ``False``, we don't move the submodule's checkout, which may leave the
996
+ parent repository in an inconsistent state.
997
+
998
+ :return:
999
+ self
1000
+
1001
+ :raise ValueError:
1002
+ If the module path existed and was not empty, or was a file.
1003
+
1004
+ :note:
1005
+ Currently the method is not atomic, and it could leave the repository in an
1006
+ inconsistent state if a sub-step fails for some reason.
1007
+ """
1008
+ if module + configuration < 1:
1009
+ raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1010
+ # END handle input
1011
+
1012
+ module_checkout_path = self._to_relative_path(self.repo, module_path)
1013
+
1014
+ # VERIFY DESTINATION
1015
+ if module_checkout_path == self.path:
1016
+ return self
1017
+ # END handle no change
1018
+
1019
+ module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path)
1020
+ if osp.isfile(module_checkout_abspath):
1021
+ raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
1022
+ # END handle target files
1023
+
1024
+ index = self.repo.index
1025
+ tekey = index.entry_key(module_checkout_path, 0)
1026
+ # if the target item already exists, fail
1027
+ if configuration and tekey in index.entries:
1028
+ raise ValueError("Index entry for target path did already exist")
1029
+ # END handle index key already there
1030
+
1031
+ # Remove existing destination.
1032
+ if module:
1033
+ if osp.exists(module_checkout_abspath):
1034
+ if len(os.listdir(module_checkout_abspath)):
1035
+ raise ValueError("Destination module directory was not empty")
1036
+ # END handle non-emptiness
1037
+
1038
+ if osp.islink(module_checkout_abspath):
1039
+ os.remove(module_checkout_abspath)
1040
+ else:
1041
+ os.rmdir(module_checkout_abspath)
1042
+ # END handle link
1043
+ else:
1044
+ # Recreate parent directories.
1045
+ # NOTE: renames() does that now.
1046
+ pass
1047
+ # END handle existence
1048
+ # END handle module
1049
+
1050
+ # Move the module into place if possible.
1051
+ cur_path = self.abspath
1052
+ renamed_module = False
1053
+ if module and osp.exists(cur_path):
1054
+ os.renames(cur_path, module_checkout_abspath)
1055
+ renamed_module = True
1056
+
1057
+ if osp.isfile(osp.join(module_checkout_abspath, ".git")):
1058
+ module_abspath = self._module_abspath(self.repo, self.path, self.name)
1059
+ self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
1060
+ # END handle git file rewrite
1061
+ # END move physical module
1062
+
1063
+ # Rename the index entry - we have to manipulate the index directly as git-mv
1064
+ # cannot be used on submodules... yeah.
1065
+ previous_sm_path = self.path
1066
+ try:
1067
+ if configuration:
1068
+ try:
1069
+ ekey = index.entry_key(self.path, 0)
1070
+ entry = index.entries[ekey]
1071
+ del index.entries[ekey]
1072
+ nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
1073
+ index.entries[tekey] = nentry
1074
+ except KeyError as e:
1075
+ raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e
1076
+ # END handle submodule doesn't exist
1077
+
1078
+ # Update configuration.
1079
+ with self.config_writer(index=index) as writer: # Auto-write.
1080
+ writer.set_value("path", module_checkout_path)
1081
+ self.path = module_checkout_path
1082
+ # END handle configuration flag
1083
+ except Exception:
1084
+ if renamed_module:
1085
+ os.renames(module_checkout_abspath, cur_path)
1086
+ # END undo module renaming
1087
+ raise
1088
+ # END handle undo rename
1089
+
1090
+ # Auto-rename submodule if its name was 'default', that is, the checkout
1091
+ # directory.
1092
+ if previous_sm_path == self.name:
1093
+ self.rename(module_checkout_path)
1094
+
1095
+ return self
1096
+
1097
+ @unbare_repo
1098
+ def remove(
1099
+ self,
1100
+ module: bool = True,
1101
+ force: bool = False,
1102
+ configuration: bool = True,
1103
+ dry_run: bool = False,
1104
+ ) -> "Submodule":
1105
+ """Remove this submodule from the repository. This will remove our entry
1106
+ from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
1107
+
1108
+ :param module:
1109
+ If ``True``, the checked out module we point to will be deleted as well. If
1110
+ that module is currently on a commit outside any branch in the remote, or if
1111
+ it is ahead of its tracking branch, or if there are modified or untracked
1112
+ files in its working tree, then the removal will fail. In case the removal
1113
+ of the repository fails for these reasons, the submodule status will not
1114
+ have been altered.
1115
+
1116
+ If this submodule has child modules of its own, these will be deleted prior
1117
+ to touching the direct submodule.
1118
+
1119
+ :param force:
1120
+ Enforces the deletion of the module even though it contains modifications.
1121
+ This basically enforces a brute-force file system based deletion.
1122
+
1123
+ :param configuration:
1124
+ If ``True``, the submodule is deleted from the configuration, otherwise it
1125
+ isn't. Although this should be enabled most of the time, this flag enables
1126
+ you to safely delete the repository of your submodule.
1127
+
1128
+ :param dry_run:
1129
+ If ``True``, we will not actually do anything, but throw the errors we would
1130
+ usually throw.
1131
+
1132
+ :return:
1133
+ self
1134
+
1135
+ :note:
1136
+ Doesn't work in bare repositories.
1137
+
1138
+ :note:
1139
+ Doesn't work atomically, as failure to remove any part of the submodule will
1140
+ leave an inconsistent state.
1141
+
1142
+ :raise git.exc.InvalidGitRepositoryError:
1143
+ Thrown if the repository cannot be deleted.
1144
+
1145
+ :raise OSError:
1146
+ If directories or files could not be removed.
1147
+ """
1148
+ if not (module or configuration):
1149
+ raise ValueError("Need to specify to delete at least the module, or the configuration")
1150
+ # END handle parameters
1151
+
1152
+ # Recursively remove children of this submodule.
1153
+ nc = 0
1154
+ for csm in self.children():
1155
+ nc += 1
1156
+ csm.remove(module, force, configuration, dry_run)
1157
+ del csm
1158
+
1159
+ if configuration and not dry_run and nc > 0:
1160
+ # Ensure we don't leave the parent repository in a dirty state, and commit
1161
+ # our changes. It's important for recursive, unforced, deletions to work as
1162
+ # expected.
1163
+ self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
1164
+ # END handle recursion
1165
+
1166
+ # DELETE REPOSITORY WORKING TREE
1167
+ ################################
1168
+ if module and self.module_exists():
1169
+ mod = self.module()
1170
+ git_dir = mod.git_dir
1171
+ if force:
1172
+ # Take the fast lane and just delete everything in our module path.
1173
+ # TODO: If we run into permission problems, we have a highly
1174
+ # inconsistent state. Delete the .git folders last, start with the
1175
+ # submodules first.
1176
+ mp = self.abspath
1177
+ method: Union[None, Callable[[PathLike], None]] = None
1178
+ if osp.islink(mp):
1179
+ method = os.remove
1180
+ elif osp.isdir(mp):
1181
+ method = rmtree
1182
+ elif osp.exists(mp):
1183
+ raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
1184
+ # END handle brutal deletion
1185
+ if not dry_run:
1186
+ assert method
1187
+ method(mp)
1188
+ # END apply deletion method
1189
+ else:
1190
+ # Verify we may delete our module.
1191
+ if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
1192
+ raise InvalidGitRepositoryError(
1193
+ "Cannot delete module at %s with any modifications, unless force is specified"
1194
+ % mod.working_tree_dir
1195
+ )
1196
+ # END check for dirt
1197
+
1198
+ # Figure out whether we have new commits compared to the remotes.
1199
+ # NOTE: If the user pulled all the time, the remote heads might not have
1200
+ # been updated, so commits coming from the remote look as if they come
1201
+ # from us. But we stay strictly read-only and don't fetch beforehand.
1202
+ for remote in mod.remotes:
1203
+ num_branches_with_new_commits = 0
1204
+ rrefs = remote.refs
1205
+ for rref in rrefs:
1206
+ num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1207
+ # END for each remote ref
1208
+ # Not a single remote branch contained all our commits.
1209
+ if len(rrefs) and num_branches_with_new_commits == len(rrefs):
1210
+ raise InvalidGitRepositoryError(
1211
+ "Cannot delete module at %s as there are new commits" % mod.working_tree_dir
1212
+ )
1213
+ # END handle new commits
1214
+ # We have to manually delete some references to allow resources to
1215
+ # be cleaned up immediately when we are done with them, because
1216
+ # Python's scoping is no more granular than the whole function (loop
1217
+ # bodies are not scopes). When the objects stay alive longer, they
1218
+ # can keep handles open. On Windows, this is a problem.
1219
+ if len(rrefs):
1220
+ del rref # skipcq: PYL-W0631
1221
+ # END handle remotes
1222
+ del rrefs
1223
+ del remote
1224
+ # END for each remote
1225
+
1226
+ # Finally delete our own submodule.
1227
+ if not dry_run:
1228
+ self._clear_cache()
1229
+ wtd = mod.working_tree_dir
1230
+ del mod # Release file-handles (Windows).
1231
+ gc.collect()
1232
+ rmtree(str(wtd))
1233
+ # END delete tree if possible
1234
+ # END handle force
1235
+
1236
+ if not dry_run and osp.isdir(git_dir):
1237
+ self._clear_cache()
1238
+ rmtree(git_dir)
1239
+ # END handle separate bare repository
1240
+ # END handle module deletion
1241
+
1242
+ # Void our data so as not to delay invalid access.
1243
+ if not dry_run:
1244
+ self._clear_cache()
1245
+
1246
+ # DELETE CONFIGURATION
1247
+ ######################
1248
+ if configuration and not dry_run:
1249
+ # First the index-entry.
1250
+ parent_index = self.repo.index
1251
+ try:
1252
+ del parent_index.entries[parent_index.entry_key(self.path, 0)]
1253
+ except KeyError:
1254
+ pass
1255
+ # END delete entry
1256
+ parent_index.write()
1257
+
1258
+ # Now git config - we need the config intact, otherwise we can't query
1259
+ # information anymore.
1260
+
1261
+ with self.repo.config_writer() as gcp_writer:
1262
+ gcp_writer.remove_section(sm_section(self.name))
1263
+
1264
+ with self.config_writer() as sc_writer:
1265
+ sc_writer.remove_section()
1266
+ # END delete configuration
1267
+
1268
+ return self
1269
+
1270
+ def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
1271
+ """Set this instance to use the given commit whose tree is supposed to
1272
+ contain the ``.gitmodules`` blob.
1273
+
1274
+ :param commit:
1275
+ Commit-ish reference pointing at the root tree, or ``None`` to always point
1276
+ to the most recent commit.
1277
+
1278
+ :param check:
1279
+ If ``True``, relatively expensive checks will be performed to verify
1280
+ validity of the submodule.
1281
+
1282
+ :raise ValueError:
1283
+ If the commit's tree didn't contain the ``.gitmodules`` blob.
1284
+
1285
+ :raise ValueError:
1286
+ If the parent commit didn't store this submodule under the current path.
1287
+
1288
+ :return:
1289
+ self
1290
+ """
1291
+ if commit is None:
1292
+ self._parent_commit = None
1293
+ return self
1294
+ # END handle None
1295
+ pcommit = self.repo.commit(commit)
1296
+ pctree = pcommit.tree
1297
+ if self.k_modules_file not in pctree:
1298
+ raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file))
1299
+ # END handle exceptions
1300
+
1301
+ prev_pc = self._parent_commit
1302
+ self._parent_commit = pcommit
1303
+
1304
+ if check:
1305
+ parser = self._config_parser(self.repo, self._parent_commit, read_only=True)
1306
+ if not parser.has_section(sm_section(self.name)):
1307
+ self._parent_commit = prev_pc
1308
+ raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit))
1309
+ # END handle submodule did not exist
1310
+ # END handle checking mode
1311
+
1312
+ # Update our sha, it could have changed.
1313
+ # If check is False, we might see a parent-commit that doesn't even contain the
1314
+ # submodule anymore. in that case, mark our sha as being NULL.
1315
+ try:
1316
+ self.binsha = pctree[str(self.path)].binsha
1317
+ except KeyError:
1318
+ self.binsha = self.NULL_BIN_SHA
1319
+
1320
+ self._clear_cache()
1321
+ return self
1322
+
1323
+ @unbare_repo
1324
+ def config_writer(
1325
+ self, index: Union["IndexFile", None] = None, write: bool = True
1326
+ ) -> SectionConstraint["SubmoduleConfigParser"]:
1327
+ """
1328
+ :return:
1329
+ A config writer instance allowing you to read and write the data belonging
1330
+ to this submodule into the ``.gitmodules`` file.
1331
+
1332
+ :param index:
1333
+ If not ``None``, an :class:`~git.index.base.IndexFile` instance which should
1334
+ be written. Defaults to the index of the :class:`Submodule`'s parent
1335
+ repository.
1336
+
1337
+ :param write:
1338
+ If ``True``, the index will be written each time a configuration value changes.
1339
+
1340
+ :note:
1341
+ The parameters allow for a more efficient writing of the index, as you can
1342
+ pass in a modified index on your own, prevent automatic writing, and write
1343
+ yourself once the whole operation is complete.
1344
+
1345
+ :raise ValueError:
1346
+ If trying to get a writer on a parent_commit which does not match the
1347
+ current head commit.
1348
+
1349
+ :raise IOError:
1350
+ If the ``.gitmodules`` file/blob could not be read.
1351
+ """
1352
+ writer = self._config_parser_constrained(read_only=False)
1353
+ if index is not None:
1354
+ writer.config._index = index
1355
+ writer.config._auto_write = write
1356
+ return writer
1357
+
1358
+ @unbare_repo
1359
+ def rename(self, new_name: str) -> "Submodule":
1360
+ """Rename this submodule.
1361
+
1362
+ :note:
1363
+ This method takes care of renaming the submodule in various places, such as:
1364
+
1365
+ * ``$parent_git_dir / config``
1366
+ * ``$working_tree_dir / .gitmodules``
1367
+ * (git >= v1.8.0: move submodule repository to new name)
1368
+
1369
+ As ``.gitmodules`` will be changed, you would need to make a commit afterwards.
1370
+ The changed ``.gitmodules`` file will already be added to the index.
1371
+
1372
+ :return:
1373
+ This :class:`Submodule` instance
1374
+ """
1375
+ if self.name == new_name:
1376
+ return self
1377
+
1378
+ # .git/config
1379
+ with self.repo.config_writer() as pw:
1380
+ # As we ourselves didn't write anything about submodules into the parent
1381
+ # .git/config, we will not require it to exist, and just ignore missing
1382
+ # entries.
1383
+ if pw.has_section(sm_section(self.name)):
1384
+ pw.rename_section(sm_section(self.name), sm_section(new_name))
1385
+
1386
+ # .gitmodules
1387
+ with self.config_writer(write=True).config as cw:
1388
+ cw.rename_section(sm_section(self.name), sm_section(new_name))
1389
+
1390
+ self._name = new_name
1391
+
1392
+ # .git/modules
1393
+ mod = self.module()
1394
+ if mod.has_separate_working_tree():
1395
+ destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
1396
+ source_dir = mod.git_dir
1397
+ # Let's be sure the submodule name is not so obviously tied to a directory.
1398
+ if str(destination_module_abspath).startswith(str(mod.git_dir)):
1399
+ tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
1400
+ os.renames(source_dir, tmp_dir)
1401
+ source_dir = tmp_dir
1402
+ # END handle self-containment
1403
+ os.renames(source_dir, destination_module_abspath)
1404
+ if mod.working_tree_dir:
1405
+ self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
1406
+ # END move separate git repository
1407
+
1408
+ return self
1409
+
1410
+ # } END edit interface
1411
+
1412
+ # { Query Interface
1413
+
1414
+ @unbare_repo
1415
+ def module(self) -> "Repo":
1416
+ """
1417
+ :return:
1418
+ :class:`~git.repo.base.Repo` instance initialized from the repository at our
1419
+ submodule path
1420
+
1421
+ :raise git.exc.InvalidGitRepositoryError:
1422
+ If a repository was not available.
1423
+ This could also mean that it was not yet initialized.
1424
+ """
1425
+ module_checkout_abspath = self.abspath
1426
+ try:
1427
+ repo = git.Repo(module_checkout_abspath)
1428
+ if repo != self.repo:
1429
+ return repo
1430
+ # END handle repo uninitialized
1431
+ except (InvalidGitRepositoryError, NoSuchPathError) as e:
1432
+ raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e
1433
+ else:
1434
+ raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath)
1435
+ # END handle exceptions
1436
+
1437
+ def module_exists(self) -> bool:
1438
+ """
1439
+ :return:
1440
+ ``True`` if our module exists and is a valid git repository.
1441
+ See the :meth:`module` method.
1442
+ """
1443
+ try:
1444
+ self.module()
1445
+ return True
1446
+ except Exception:
1447
+ return False
1448
+ # END handle exception
1449
+
1450
+ def exists(self) -> bool:
1451
+ """
1452
+ :return:
1453
+ ``True`` if the submodule exists, ``False`` otherwise.
1454
+ Please note that a submodule may exist (in the ``.gitmodules`` file) even
1455
+ though its module doesn't exist on disk.
1456
+ """
1457
+ # Keep attributes for later, and restore them if we have no valid data.
1458
+ # This way we do not actually alter the state of the object.
1459
+ loc = locals()
1460
+ for attr in self._cache_attrs:
1461
+ try:
1462
+ if hasattr(self, attr):
1463
+ loc[attr] = getattr(self, attr)
1464
+ # END if we have the attribute cache
1465
+ except (cp.NoSectionError, ValueError):
1466
+ # On PY3, this can happen apparently... don't know why this doesn't
1467
+ # happen on PY2.
1468
+ pass
1469
+ # END for each attr
1470
+ self._clear_cache()
1471
+
1472
+ try:
1473
+ try:
1474
+ self.path # noqa: B018
1475
+ return True
1476
+ except Exception:
1477
+ return False
1478
+ # END handle exceptions
1479
+ finally:
1480
+ for attr in self._cache_attrs:
1481
+ if attr in loc:
1482
+ setattr(self, attr, loc[attr])
1483
+ # END if we have a cache
1484
+ # END reapply each attribute
1485
+ # END handle object state consistency
1486
+
1487
+ @property
1488
+ def branch(self) -> "Head":
1489
+ """
1490
+ :return:
1491
+ The branch instance that we are to checkout
1492
+
1493
+ :raise git.exc.InvalidGitRepositoryError:
1494
+ If our module is not yet checked out.
1495
+ """
1496
+ return mkhead(self.module(), self._branch_path)
1497
+
1498
+ @property
1499
+ def branch_path(self) -> PathLike:
1500
+ """
1501
+ :return:
1502
+ Full repository-relative path as string to the branch we would checkout from
1503
+ the remote and track
1504
+ """
1505
+ return self._branch_path
1506
+
1507
+ @property
1508
+ def branch_name(self) -> str:
1509
+ """
1510
+ :return:
1511
+ The name of the branch, which is the shortest possible branch name
1512
+ """
1513
+ # Use an instance method, for this we create a temporary Head instance which
1514
+ # uses a repository that is available at least (it makes no difference).
1515
+ return git.Head(self.repo, self._branch_path).name
1516
+
1517
+ @property
1518
+ def url(self) -> str:
1519
+ """:return: The url to the repository our submodule's repository refers to"""
1520
+ return self._url
1521
+
1522
+ @property
1523
+ def parent_commit(self) -> "Commit":
1524
+ """
1525
+ :return:
1526
+ :class:`~git.objects.commit.Commit` instance with the tree containing the
1527
+ ``.gitmodules`` file
1528
+
1529
+ :note:
1530
+ Will always point to the current head's commit if it was not set explicitly.
1531
+ """
1532
+ if self._parent_commit is None:
1533
+ return self.repo.commit()
1534
+ return self._parent_commit
1535
+
1536
+ @property
1537
+ def name(self) -> str:
1538
+ """
1539
+ :return:
1540
+ The name of this submodule. It is used to identify it within the
1541
+ ``.gitmodules`` file.
1542
+
1543
+ :note:
1544
+ By default, this is the name is the path at which to find the submodule, but
1545
+ in GitPython it should be a unique identifier similar to the identifiers
1546
+ used for remotes, which allows to change the path of the submodule easily.
1547
+ """
1548
+ return self._name
1549
+
1550
+ def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
1551
+ """
1552
+ :return:
1553
+ ConfigReader instance which allows you to query the configuration values of
1554
+ this submodule, as provided by the ``.gitmodules`` file.
1555
+
1556
+ :note:
1557
+ The config reader will actually read the data directly from the repository
1558
+ and thus does not need nor care about your working tree.
1559
+
1560
+ :note:
1561
+ Should be cached by the caller and only kept as long as needed.
1562
+
1563
+ :raise IOError:
1564
+ If the ``.gitmodules`` file/blob could not be read.
1565
+ """
1566
+ return self._config_parser_constrained(read_only=True)
1567
+
1568
+ def children(self) -> IterableList["Submodule"]:
1569
+ """
1570
+ :return:
1571
+ IterableList(Submodule, ...) An iterable list of :class:`Submodule`
1572
+ instances which are children of this submodule or 0 if the submodule is not
1573
+ checked out.
1574
+ """
1575
+ return self._get_intermediate_items(self)
1576
+
1577
+ # } END query interface
1578
+
1579
+ # { Iterable Interface
1580
+
1581
+ @classmethod
1582
+ def iter_items(
1583
+ cls,
1584
+ repo: "Repo",
1585
+ parent_commit: Union[Commit_ish, str] = "HEAD",
1586
+ *args: Any,
1587
+ **kwargs: Any,
1588
+ ) -> Iterator["Submodule"]:
1589
+ """
1590
+ :return:
1591
+ Iterator yielding :class:`Submodule` instances available in the given
1592
+ repository
1593
+ """
1594
+ try:
1595
+ pc = repo.commit(parent_commit) # Parent commit instance
1596
+ parser = cls._config_parser(repo, pc, read_only=True)
1597
+ except (IOError, BadName):
1598
+ return
1599
+ # END handle empty iterator
1600
+
1601
+ for sms in parser.sections():
1602
+ n = sm_name(sms)
1603
+ p = parser.get(sms, "path")
1604
+ u = parser.get(sms, "url")
1605
+ b = cls.k_head_default
1606
+ if parser.has_option(sms, cls.k_head_option):
1607
+ b = str(parser.get(sms, cls.k_head_option))
1608
+ # END handle optional information
1609
+
1610
+ # Get the binsha.
1611
+ index = repo.index
1612
+ try:
1613
+ rt = pc.tree # Root tree
1614
+ sm = rt[p]
1615
+ except KeyError:
1616
+ # Try the index, maybe it was just added.
1617
+ try:
1618
+ entry = index.entries[index.entry_key(p, 0)]
1619
+ sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
1620
+ except KeyError:
1621
+ # The submodule doesn't exist, probably it wasn't removed from the
1622
+ # .gitmodules file.
1623
+ continue
1624
+ # END handle keyerror
1625
+ # END handle critical error
1626
+
1627
+ # Make sure we are looking at a submodule object.
1628
+ if type(sm) is not git.objects.submodule.base.Submodule:
1629
+ continue
1630
+
1631
+ # Fill in remaining info - saves time as it doesn't have to be parsed again.
1632
+ sm._name = n
1633
+ if pc != repo.commit():
1634
+ sm._parent_commit = pc
1635
+ # END set only if not most recent!
1636
+ sm._branch_path = git.Head.to_full_path(b)
1637
+ sm._url = u
1638
+
1639
+ yield sm
1640
+ # END for each section
1641
+
1642
+ # } END iterable interface
.venv/lib/python3.11/site-packages/git/objects/submodule/root.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["RootModule", "RootUpdateProgress"]
5
+
6
+ import logging
7
+
8
+ import git
9
+ from git.exc import InvalidGitRepositoryError
10
+
11
+ from .base import Submodule, UpdateProgress
12
+ from .util import find_first_remote_branch
13
+
14
+ # typing -------------------------------------------------------------------
15
+
16
+ from typing import TYPE_CHECKING, Union
17
+
18
+ from git.types import Commit_ish
19
+
20
+ if TYPE_CHECKING:
21
+ from git.repo import Repo
22
+ from git.util import IterableList
23
+
24
+ # ----------------------------------------------------------------------------
25
+
26
+ _logger = logging.getLogger(__name__)
27
+
28
+
29
+ class RootUpdateProgress(UpdateProgress):
30
+ """Utility class which adds more opcodes to
31
+ :class:`~git.objects.submodule.base.UpdateProgress`."""
32
+
33
+ REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
34
+ 1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
35
+ ]
36
+ _num_op_codes = UpdateProgress._num_op_codes + 4
37
+
38
+ __slots__ = ()
39
+
40
+
41
+ BEGIN = RootUpdateProgress.BEGIN
42
+ END = RootUpdateProgress.END
43
+ REMOVE = RootUpdateProgress.REMOVE
44
+ BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE
45
+ URLCHANGE = RootUpdateProgress.URLCHANGE
46
+ PATHCHANGE = RootUpdateProgress.PATHCHANGE
47
+
48
+
49
+ class RootModule(Submodule):
50
+ """A (virtual) root of all submodules in the given repository.
51
+
52
+ This can be used to more easily traverse all submodules of the
53
+ superproject (master repository).
54
+ """
55
+
56
+ __slots__ = ()
57
+
58
+ k_root_name = "__ROOT__"
59
+
60
+ def __init__(self, repo: "Repo") -> None:
61
+ # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
62
+ super().__init__(
63
+ repo,
64
+ binsha=self.NULL_BIN_SHA,
65
+ mode=self.k_default_mode,
66
+ path="",
67
+ name=self.k_root_name,
68
+ parent_commit=repo.head.commit,
69
+ url="",
70
+ branch_path=git.Head.to_full_path(self.k_head_default),
71
+ )
72
+
73
+ def _clear_cache(self) -> None:
74
+ """May not do anything."""
75
+ pass
76
+
77
+ # { Interface
78
+
79
+ def update( # type: ignore[override]
80
+ self,
81
+ previous_commit: Union[Commit_ish, str, None] = None,
82
+ recursive: bool = True,
83
+ force_remove: bool = False,
84
+ init: bool = True,
85
+ to_latest_revision: bool = False,
86
+ progress: Union[None, "RootUpdateProgress"] = None,
87
+ dry_run: bool = False,
88
+ force_reset: bool = False,
89
+ keep_going: bool = False,
90
+ ) -> "RootModule":
91
+ """Update the submodules of this repository to the current HEAD commit.
92
+
93
+ This method behaves smartly by determining changes of the path of a submodule's
94
+ repository, next to changes to the to-be-checked-out commit or the branch to be
95
+ checked out. This works if the submodule's ID does not change.
96
+
97
+ Additionally it will detect addition and removal of submodules, which will be
98
+ handled gracefully.
99
+
100
+ :param previous_commit:
101
+ If set to a commit-ish, the commit we should use as the previous commit the
102
+ HEAD pointed to before it was set to the commit it points to now.
103
+ If ``None``, it defaults to ``HEAD@{1}`` otherwise.
104
+
105
+ :param recursive:
106
+ If ``True``, the children of submodules will be updated as well using the
107
+ same technique.
108
+
109
+ :param force_remove:
110
+ If submodules have been deleted, they will be forcibly removed. Otherwise
111
+ the update may fail if a submodule's repository cannot be deleted as changes
112
+ have been made to it.
113
+ (See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
114
+ for more information.)
115
+
116
+ :param init:
117
+ If we encounter a new module which would need to be initialized, then do it.
118
+
119
+ :param to_latest_revision:
120
+ If ``True``, instead of checking out the revision pointed to by this
121
+ submodule's sha, the checked out tracking branch will be merged with the
122
+ latest remote branch fetched from the repository's origin.
123
+
124
+ Unless `force_reset` is specified, a local tracking branch will never be
125
+ reset into its past, therefore the remote branch must be in the future for
126
+ this to have an effect.
127
+
128
+ :param force_reset:
129
+ If ``True``, submodules may checkout or reset their branch even if the
130
+ repository has pending changes that would be overwritten, or if the local
131
+ tracking branch is in the future of the remote tracking branch and would be
132
+ reset into its past.
133
+
134
+ :param progress:
135
+ :class:`RootUpdateProgress` instance, or ``None`` if no progress should be
136
+ sent.
137
+
138
+ :param dry_run:
139
+ If ``True``, operations will not actually be performed. Progress messages
140
+ will change accordingly to indicate the WOULD DO state of the operation.
141
+
142
+ :param keep_going:
143
+ If ``True``, we will ignore but log all errors, and keep going recursively.
144
+ Unless `dry_run` is set as well, `keep_going` could cause
145
+ subsequent/inherited errors you wouldn't see otherwise.
146
+ In conjunction with `dry_run`, this can be useful to anticipate all errors
147
+ when updating submodules.
148
+
149
+ :return:
150
+ self
151
+ """
152
+ if self.repo.bare:
153
+ raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
154
+ # END handle bare
155
+
156
+ if progress is None:
157
+ progress = RootUpdateProgress()
158
+ # END ensure progress is set
159
+
160
+ prefix = ""
161
+ if dry_run:
162
+ prefix = "DRY-RUN: "
163
+
164
+ repo = self.repo
165
+
166
+ try:
167
+ # SETUP BASE COMMIT
168
+ ###################
169
+ cur_commit = repo.head.commit
170
+ if previous_commit is None:
171
+ try:
172
+ previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha)
173
+ if previous_commit.binsha == previous_commit.NULL_BIN_SHA:
174
+ raise IndexError
175
+ # END handle initial commit
176
+ except IndexError:
177
+ # In new repositories, there is no previous commit.
178
+ previous_commit = cur_commit
179
+ # END exception handling
180
+ else:
181
+ previous_commit = repo.commit(previous_commit) # Obtain commit object.
182
+ # END handle previous commit
183
+
184
+ psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
185
+ sms: "IterableList[Submodule]" = self.list_items(repo)
186
+ spsms = set(psms)
187
+ ssms = set(sms)
188
+
189
+ # HANDLE REMOVALS
190
+ ###################
191
+ rrsm = spsms - ssms
192
+ len_rrsm = len(rrsm)
193
+
194
+ for i, rsm in enumerate(rrsm):
195
+ op = REMOVE
196
+ if i == 0:
197
+ op |= BEGIN
198
+ # END handle begin
199
+
200
+ # Fake it into thinking its at the current commit to allow deletion
201
+ # of previous module. Trigger the cache to be updated before that.
202
+ progress.update(
203
+ op,
204
+ i,
205
+ len_rrsm,
206
+ prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath),
207
+ )
208
+ rsm._parent_commit = repo.head.commit
209
+ rsm.remove(
210
+ configuration=False,
211
+ module=True,
212
+ force=force_remove,
213
+ dry_run=dry_run,
214
+ )
215
+
216
+ if i == len_rrsm - 1:
217
+ op |= END
218
+ # END handle end
219
+ progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name)
220
+ # END for each removed submodule
221
+
222
+ # HANDLE PATH RENAMES
223
+ #####################
224
+ # URL changes + branch changes.
225
+ csms = spsms & ssms
226
+ len_csms = len(csms)
227
+ for i, csm in enumerate(csms):
228
+ psm: "Submodule" = psms[csm.name]
229
+ sm: "Submodule" = sms[csm.name]
230
+
231
+ # PATH CHANGES
232
+ ##############
233
+ if sm.path != psm.path and psm.module_exists():
234
+ progress.update(
235
+ BEGIN | PATHCHANGE,
236
+ i,
237
+ len_csms,
238
+ prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath),
239
+ )
240
+ # Move the module to the new path.
241
+ if not dry_run:
242
+ psm.move(sm.path, module=True, configuration=False)
243
+ # END handle dry_run
244
+ progress.update(
245
+ END | PATHCHANGE,
246
+ i,
247
+ len_csms,
248
+ prefix + "Done moving repository of submodule %r" % sm.name,
249
+ )
250
+ # END handle path changes
251
+
252
+ if sm.module_exists():
253
+ # HANDLE URL CHANGE
254
+ ###################
255
+ if sm.url != psm.url:
256
+ # Add the new remote, remove the old one.
257
+ # This way, if the url just changes, the commits will not have
258
+ # to be re-retrieved.
259
+ nn = "__new_origin__"
260
+ smm = sm.module()
261
+ rmts = smm.remotes
262
+
263
+ # Don't do anything if we already have the url we search in
264
+ # place.
265
+ if len([r for r in rmts if r.url == sm.url]) == 0:
266
+ progress.update(
267
+ BEGIN | URLCHANGE,
268
+ i,
269
+ len_csms,
270
+ prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url),
271
+ )
272
+
273
+ if not dry_run:
274
+ assert nn not in [r.name for r in rmts]
275
+ smr = smm.create_remote(nn, sm.url)
276
+ smr.fetch(progress=progress)
277
+
278
+ # If we have a tracking branch, it should be available
279
+ # in the new remote as well.
280
+ if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
281
+ raise ValueError(
282
+ "Submodule branch named %r was not available in new submodule remote at %r"
283
+ % (sm.branch_name, sm.url)
284
+ )
285
+ # END head is not detached
286
+
287
+ # Now delete the changed one.
288
+ rmt_for_deletion = None
289
+ for remote in rmts:
290
+ if remote.url == psm.url:
291
+ rmt_for_deletion = remote
292
+ break
293
+ # END if urls match
294
+ # END for each remote
295
+
296
+ # If we didn't find a matching remote, but have exactly
297
+ # one, we can safely use this one.
298
+ if rmt_for_deletion is None:
299
+ if len(rmts) == 1:
300
+ rmt_for_deletion = rmts[0]
301
+ else:
302
+ # If we have not found any remote with the
303
+ # original URL we may not have a name. This is a
304
+ # special case, and its okay to fail here.
305
+ # Alternatively we could just generate a unique
306
+ # name and leave all existing ones in place.
307
+ raise InvalidGitRepositoryError(
308
+ "Couldn't find original remote-repo at url %r" % psm.url
309
+ )
310
+ # END handle one single remote
311
+ # END handle check we found a remote
312
+
313
+ orig_name = rmt_for_deletion.name
314
+ smm.delete_remote(rmt_for_deletion)
315
+ # NOTE: Currently we leave tags from the deleted remotes
316
+ # as well as separate tracking branches in the possibly
317
+ # totally changed repository (someone could have changed
318
+ # the url to another project). At some point, one might
319
+ # want to clean it up, but the danger is high to remove
320
+ # stuff the user has added explicitly.
321
+
322
+ # Rename the new remote back to what it was.
323
+ smr.rename(orig_name)
324
+
325
+ # Early on, we verified that the our current tracking
326
+ # branch exists in the remote. Now we have to ensure
327
+ # that the sha we point to is still contained in the new
328
+ # remote tracking branch.
329
+ smsha = sm.binsha
330
+ found = False
331
+ rref = smr.refs[self.branch_name]
332
+ for c in rref.commit.traverse():
333
+ if c.binsha == smsha:
334
+ found = True
335
+ break
336
+ # END traverse all commits in search for sha
337
+ # END for each commit
338
+
339
+ if not found:
340
+ # Adjust our internal binsha to use the one of the
341
+ # remote this way, it will be checked out in the
342
+ # next step. This will change the submodule relative
343
+ # to us, so the user will be able to commit the
344
+ # change easily.
345
+ _logger.warning(
346
+ "Current sha %s was not contained in the tracking\
347
+ branch at the new remote, setting it the the remote's tracking branch",
348
+ sm.hexsha,
349
+ )
350
+ sm.binsha = rref.commit.binsha
351
+ # END reset binsha
352
+
353
+ # NOTE: All checkout is performed by the base
354
+ # implementation of update.
355
+ # END handle dry_run
356
+ progress.update(
357
+ END | URLCHANGE,
358
+ i,
359
+ len_csms,
360
+ prefix + "Done adjusting url of submodule %r" % (sm.name),
361
+ )
362
+ # END skip remote handling if new url already exists in module
363
+ # END handle url
364
+
365
+ # HANDLE PATH CHANGES
366
+ #####################
367
+ if sm.branch_path != psm.branch_path:
368
+ # Finally, create a new tracking branch which tracks the new
369
+ # remote branch.
370
+ progress.update(
371
+ BEGIN | BRANCHCHANGE,
372
+ i,
373
+ len_csms,
374
+ prefix
375
+ + "Changing branch of submodule %r from %s to %s"
376
+ % (sm.name, psm.branch_path, sm.branch_path),
377
+ )
378
+ if not dry_run:
379
+ smm = sm.module()
380
+ smmr = smm.remotes
381
+ # As the branch might not exist yet, we will have to fetch
382
+ # all remotes to be sure...
383
+ for remote in smmr:
384
+ remote.fetch(progress=progress)
385
+ # END for each remote
386
+
387
+ try:
388
+ tbr = git.Head.create(
389
+ smm,
390
+ sm.branch_name,
391
+ logmsg="branch: Created from HEAD",
392
+ )
393
+ except OSError:
394
+ # ...or reuse the existing one.
395
+ tbr = git.Head(smm, sm.branch_path)
396
+ # END ensure tracking branch exists
397
+
398
+ tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
399
+ # NOTE: All head-resetting is done in the base
400
+ # implementation of update but we will have to checkout the
401
+ # new branch here. As it still points to the currently
402
+ # checked out commit, we don't do any harm.
403
+ # As we don't want to update working-tree or index, changing
404
+ # the ref is all there is to do.
405
+ smm.head.reference = tbr
406
+ # END handle dry_run
407
+
408
+ progress.update(
409
+ END | BRANCHCHANGE,
410
+ i,
411
+ len_csms,
412
+ prefix + "Done changing branch of submodule %r" % sm.name,
413
+ )
414
+ # END handle branch
415
+ # END handle
416
+ # END for each common submodule
417
+ except Exception as err:
418
+ if not keep_going:
419
+ raise
420
+ _logger.error(str(err))
421
+ # END handle keep_going
422
+
423
+ # FINALLY UPDATE ALL ACTUAL SUBMODULES
424
+ ######################################
425
+ for sm in sms:
426
+ # Update the submodule using the default method.
427
+ sm.update(
428
+ recursive=False,
429
+ init=init,
430
+ to_latest_revision=to_latest_revision,
431
+ progress=progress,
432
+ dry_run=dry_run,
433
+ force=force_reset,
434
+ keep_going=keep_going,
435
+ )
436
+
437
+ # Update recursively depth first - question is which inconsistent state will
438
+ # be better in case it fails somewhere. Defective branch or defective depth.
439
+ # The RootSubmodule type will never process itself, which was done in the
440
+ # previous expression.
441
+ if recursive:
442
+ # The module would exist by now if we are not in dry_run mode.
443
+ if sm.module_exists():
444
+ type(self)(sm.module()).update(
445
+ recursive=True,
446
+ force_remove=force_remove,
447
+ init=init,
448
+ to_latest_revision=to_latest_revision,
449
+ progress=progress,
450
+ dry_run=dry_run,
451
+ force_reset=force_reset,
452
+ keep_going=keep_going,
453
+ )
454
+ # END handle dry_run
455
+ # END handle recursive
456
+ # END for each submodule to update
457
+
458
+ return self
459
+
460
+ def module(self) -> "Repo":
461
+ """:return: The actual repository containing the submodules"""
462
+ return self.repo
463
+
464
+ # } END interface
465
+
466
+
467
+ # } END classes
.venv/lib/python3.11/site-packages/git/objects/submodule/util.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = [
5
+ "sm_section",
6
+ "sm_name",
7
+ "mkhead",
8
+ "find_first_remote_branch",
9
+ "SubmoduleConfigParser",
10
+ ]
11
+
12
+ from io import BytesIO
13
+ import weakref
14
+
15
+ import git
16
+ from git.config import GitConfigParser
17
+ from git.exc import InvalidGitRepositoryError
18
+
19
+ # typing -----------------------------------------------------------------------
20
+
21
+ from typing import Any, Sequence, TYPE_CHECKING, Union
22
+
23
+ from git.types import PathLike
24
+
25
+ if TYPE_CHECKING:
26
+ from weakref import ReferenceType
27
+
28
+ from git.refs import Head, RemoteReference
29
+ from git.remote import Remote
30
+ from git.repo import Repo
31
+
32
+ from .base import Submodule
33
+
34
+ # { Utilities
35
+
36
+
37
+ def sm_section(name: str) -> str:
38
+ """:return: Section title used in ``.gitmodules`` configuration file"""
39
+ return f'submodule "{name}"'
40
+
41
+
42
+ def sm_name(section: str) -> str:
43
+ """:return: Name of the submodule as parsed from the section name"""
44
+ section = section.strip()
45
+ return section[11:-1]
46
+
47
+
48
+ def mkhead(repo: "Repo", path: PathLike) -> "Head":
49
+ """:return: New branch/head instance"""
50
+ return git.Head(repo, git.Head.to_full_path(path))
51
+
52
+
53
+ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
54
+ """Find the remote branch matching the name of the given branch or raise
55
+ :exc:`~git.exc.InvalidGitRepositoryError`."""
56
+ for remote in remotes:
57
+ try:
58
+ return remote.refs[branch_name]
59
+ except IndexError:
60
+ continue
61
+ # END exception handling
62
+ # END for remote
63
+ raise InvalidGitRepositoryError("Didn't find remote branch '%r' in any of the given remotes" % branch_name)
64
+
65
+
66
+ # } END utilities
67
+
68
+ # { Classes
69
+
70
+
71
+ class SubmoduleConfigParser(GitConfigParser):
72
+ """Catches calls to :meth:`~git.config.GitConfigParser.write`, and updates the
73
+ ``.gitmodules`` blob in the index with the new data, if we have written into a
74
+ stream.
75
+
76
+ Otherwise it would add the local file to the index to make it correspond with the
77
+ working tree. Additionally, the cache must be cleared.
78
+
79
+ Please note that no mutating method will work in bare mode.
80
+ """
81
+
82
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
83
+ self._smref: Union["ReferenceType[Submodule]", None] = None
84
+ self._index = None
85
+ self._auto_write = True
86
+ super().__init__(*args, **kwargs)
87
+
88
+ # { Interface
89
+ def set_submodule(self, submodule: "Submodule") -> None:
90
+ """Set this instance's submodule. It must be called before the first write
91
+ operation begins."""
92
+ self._smref = weakref.ref(submodule)
93
+
94
+ def flush_to_index(self) -> None:
95
+ """Flush changes in our configuration file to the index."""
96
+ assert self._smref is not None
97
+ # Should always have a file here.
98
+ assert not isinstance(self._file_or_files, BytesIO)
99
+
100
+ sm = self._smref()
101
+ if sm is not None:
102
+ index = self._index
103
+ if index is None:
104
+ index = sm.repo.index
105
+ # END handle index
106
+ index.add([sm.k_modules_file], write=self._auto_write)
107
+ sm._clear_cache()
108
+ # END handle weakref
109
+
110
+ # } END interface
111
+
112
+ # { Overridden Methods
113
+ def write(self) -> None: # type: ignore[override]
114
+ rval: None = super().write()
115
+ self.flush_to_index()
116
+ return rval
117
+
118
+ # END overridden methods
119
+
120
+
121
+ # } END classes
.venv/lib/python3.11/site-packages/git/objects/tag.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Provides an :class:`~git.objects.base.Object`-based type for annotated tags.
7
+
8
+ This defines the :class:`TagObject` class, which represents annotated tags.
9
+ For lightweight tags, see the :mod:`git.refs.tag` module.
10
+ """
11
+
12
+ __all__ = ["TagObject"]
13
+
14
+ import sys
15
+
16
+ from git.compat import defenc
17
+ from git.util import Actor, hex_to_bin
18
+
19
+ from . import base
20
+ from .util import get_object_type_by_name, parse_actor_and_date
21
+
22
+ # typing ----------------------------------------------
23
+
24
+ from typing import List, TYPE_CHECKING, Union
25
+
26
+ if sys.version_info >= (3, 8):
27
+ from typing import Literal
28
+ else:
29
+ from typing_extensions import Literal
30
+
31
+ if TYPE_CHECKING:
32
+ from git.repo import Repo
33
+
34
+ from .blob import Blob
35
+ from .commit import Commit
36
+ from .tree import Tree
37
+
38
+ # ---------------------------------------------------
39
+
40
+
41
+ class TagObject(base.Object):
42
+ """Annotated (i.e. non-lightweight) tag carrying additional information about an
43
+ object we are pointing to.
44
+
45
+ See :manpage:`gitglossary(7)` on "tag object":
46
+ https://git-scm.com/docs/gitglossary#def_tag_object
47
+ """
48
+
49
+ type: Literal["tag"] = "tag"
50
+
51
+ __slots__ = (
52
+ "object",
53
+ "tag",
54
+ "tagger",
55
+ "tagged_date",
56
+ "tagger_tz_offset",
57
+ "message",
58
+ )
59
+
60
+ def __init__(
61
+ self,
62
+ repo: "Repo",
63
+ binsha: bytes,
64
+ object: Union[None, base.Object] = None,
65
+ tag: Union[None, str] = None,
66
+ tagger: Union[None, Actor] = None,
67
+ tagged_date: Union[int, None] = None,
68
+ tagger_tz_offset: Union[int, None] = None,
69
+ message: Union[str, None] = None,
70
+ ) -> None: # @ReservedAssignment
71
+ """Initialize a tag object with additional data.
72
+
73
+ :param repo:
74
+ Repository this object is located in.
75
+
76
+ :param binsha:
77
+ 20 byte SHA1.
78
+
79
+ :param object:
80
+ :class:`~git.objects.base.Object` instance of object we are pointing to.
81
+
82
+ :param tag:
83
+ Name of this tag.
84
+
85
+ :param tagger:
86
+ :class:`~git.util.Actor` identifying the tagger.
87
+
88
+ :param tagged_date: int_seconds_since_epoch
89
+ The DateTime of the tag creation.
90
+ Use :func:`time.gmtime` to convert it into a different format.
91
+
92
+ :param tagger_tz_offset: int_seconds_west_of_utc
93
+ The timezone that the `tagged_date` is in, in a format similar to
94
+ :attr:`time.altzone`.
95
+ """
96
+ super().__init__(repo, binsha)
97
+ if object is not None:
98
+ self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object
99
+ if tag is not None:
100
+ self.tag = tag
101
+ if tagger is not None:
102
+ self.tagger = tagger
103
+ if tagged_date is not None:
104
+ self.tagged_date = tagged_date
105
+ if tagger_tz_offset is not None:
106
+ self.tagger_tz_offset = tagger_tz_offset
107
+ if message is not None:
108
+ self.message = message
109
+
110
+ def _set_cache_(self, attr: str) -> None:
111
+ """Cache all our attributes at once."""
112
+ if attr in TagObject.__slots__:
113
+ ostream = self.repo.odb.stream(self.binsha)
114
+ lines: List[str] = ostream.read().decode(defenc, "replace").splitlines()
115
+
116
+ _obj, hexsha = lines[0].split(" ")
117
+ _type_token, type_name = lines[1].split(" ")
118
+ object_type = get_object_type_by_name(type_name.encode("ascii"))
119
+ self.object = object_type(self.repo, hex_to_bin(hexsha))
120
+
121
+ self.tag = lines[2][4:] # tag <tag name>
122
+
123
+ if len(lines) > 3:
124
+ tagger_info = lines[3] # tagger <actor> <date>
125
+ (
126
+ self.tagger,
127
+ self.tagged_date,
128
+ self.tagger_tz_offset,
129
+ ) = parse_actor_and_date(tagger_info)
130
+
131
+ # Line 4 empty - it could mark the beginning of the next header.
132
+ # In case there really is no message, it would not exist.
133
+ # Otherwise a newline separates header from message.
134
+ if len(lines) > 5:
135
+ self.message = "\n".join(lines[5:])
136
+ else:
137
+ self.message = ""
138
+ # END check our attributes
139
+ else:
140
+ super()._set_cache_(attr)
.venv/lib/python3.11/site-packages/git/objects/tree.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ __all__ = ["TreeModifier", "Tree"]
7
+
8
+ import sys
9
+
10
+ import git.diff as git_diff
11
+ from git.util import IterableList, join_path, to_bin_sha
12
+
13
+ from . import util
14
+ from .base import IndexObjUnion, IndexObject
15
+ from .blob import Blob
16
+ from .fun import tree_entries_from_data, tree_to_stream
17
+ from .submodule.base import Submodule
18
+
19
+ # typing -------------------------------------------------
20
+
21
+ from typing import (
22
+ Any,
23
+ Callable,
24
+ Dict,
25
+ Iterable,
26
+ Iterator,
27
+ List,
28
+ Tuple,
29
+ TYPE_CHECKING,
30
+ Type,
31
+ Union,
32
+ cast,
33
+ )
34
+
35
+ if sys.version_info >= (3, 8):
36
+ from typing import Literal
37
+ else:
38
+ from typing_extensions import Literal
39
+
40
+ from git.types import PathLike
41
+
42
+ if TYPE_CHECKING:
43
+ from io import BytesIO
44
+
45
+ from git.repo import Repo
46
+
47
+ TreeCacheTup = Tuple[bytes, int, str]
48
+
49
+ TraversedTreeTup = Union[Tuple[Union["Tree", None], IndexObjUnion, Tuple["Submodule", "Submodule"]]]
50
+
51
+ # --------------------------------------------------------
52
+
53
+
54
+ def cmp(a: str, b: str) -> int:
55
+ return (a > b) - (a < b)
56
+
57
+
58
+ class TreeModifier:
59
+ """A utility class providing methods to alter the underlying cache in a list-like
60
+ fashion.
61
+
62
+ Once all adjustments are complete, the :attr:`_cache`, which really is a reference
63
+ to the cache of a tree, will be sorted. This ensures it will be in a serializable
64
+ state.
65
+ """
66
+
67
+ __slots__ = ("_cache",)
68
+
69
+ def __init__(self, cache: List[TreeCacheTup]) -> None:
70
+ self._cache = cache
71
+
72
+ def _index_by_name(self, name: str) -> int:
73
+ """:return: index of an item with name, or -1 if not found"""
74
+ for i, t in enumerate(self._cache):
75
+ if t[2] == name:
76
+ return i
77
+ # END found item
78
+ # END for each item in cache
79
+ return -1
80
+
81
+ # { Interface
82
+ def set_done(self) -> "TreeModifier":
83
+ """Call this method once you are done modifying the tree information.
84
+
85
+ This may be called several times, but be aware that each call will cause a sort
86
+ operation.
87
+
88
+ :return:
89
+ self
90
+ """
91
+ self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
92
+ return self
93
+
94
+ # } END interface
95
+
96
+ # { Mutators
97
+ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeModifier":
98
+ """Add the given item to the tree.
99
+
100
+ If an item with the given name already exists, nothing will be done, but a
101
+ :exc:`ValueError` will be raised if the sha and mode of the existing item do not
102
+ match the one you add, unless `force` is ``True``.
103
+
104
+ :param sha:
105
+ The 20 or 40 byte sha of the item to add.
106
+
107
+ :param mode:
108
+ :class:`int` representing the stat-compatible mode of the item.
109
+
110
+ :param force:
111
+ If ``True``, an item with your name and information will overwrite any
112
+ existing item with the same name, no matter which information it has.
113
+
114
+ :return:
115
+ self
116
+ """
117
+ if "/" in name:
118
+ raise ValueError("Name must not contain '/' characters")
119
+ if (mode >> 12) not in Tree._map_id_to_type:
120
+ raise ValueError("Invalid object type according to mode %o" % mode)
121
+
122
+ sha = to_bin_sha(sha)
123
+ index = self._index_by_name(name)
124
+
125
+ item = (sha, mode, name)
126
+
127
+ if index == -1:
128
+ self._cache.append(item)
129
+ else:
130
+ if force:
131
+ self._cache[index] = item
132
+ else:
133
+ ex_item = self._cache[index]
134
+ if ex_item[0] != sha or ex_item[1] != mode:
135
+ raise ValueError("Item %r existed with different properties" % name)
136
+ # END handle mismatch
137
+ # END handle force
138
+ # END handle name exists
139
+ return self
140
+
141
+ def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
142
+ """Add the given item to the tree. Its correctness is assumed, so it is the
143
+ caller's responsibility to ensure that the input is correct.
144
+
145
+ For more information on the parameters, see :meth:`add`.
146
+
147
+ :param binsha:
148
+ 20 byte binary sha.
149
+ """
150
+ assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str)
151
+ tree_cache = (binsha, mode, name)
152
+
153
+ self._cache.append(tree_cache)
154
+
155
+ def __delitem__(self, name: str) -> None:
156
+ """Delete an item with the given name if it exists."""
157
+ index = self._index_by_name(name)
158
+ if index > -1:
159
+ del self._cache[index]
160
+
161
+ # } END mutators
162
+
163
+
164
+ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
165
+ R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
166
+ other :class:`Tree`\s.
167
+
168
+ See :manpage:`gitglossary(7)` on "tree object":
169
+ https://git-scm.com/docs/gitglossary#def_tree_object
170
+
171
+ Subscripting is supported, as with a list or dict:
172
+
173
+ * Access a specific blob using the ``tree["filename"]`` notation.
174
+ * You may likewise access by index, like ``blob = tree[0]``.
175
+ """
176
+
177
+ type: Literal["tree"] = "tree"
178
+
179
+ __slots__ = ("_cache",)
180
+
181
+ # Actual integer IDs for comparison.
182
+ commit_id = 0o16 # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link.
183
+ blob_id = 0o10
184
+ symlink_id = 0o12
185
+ tree_id = 0o04
186
+
187
+ _map_id_to_type: Dict[int, Type[IndexObjUnion]] = {
188
+ commit_id: Submodule,
189
+ blob_id: Blob,
190
+ symlink_id: Blob,
191
+ # Tree ID added once Tree is defined.
192
+ }
193
+
194
+ def __init__(
195
+ self,
196
+ repo: "Repo",
197
+ binsha: bytes,
198
+ mode: int = tree_id << 12,
199
+ path: Union[PathLike, None] = None,
200
+ ):
201
+ super().__init__(repo, binsha, mode, path)
202
+
203
+ @classmethod
204
+ def _get_intermediate_items(
205
+ cls,
206
+ index_object: IndexObjUnion,
207
+ ) -> Union[Tuple["Tree", ...], Tuple[()]]:
208
+ if index_object.type == "tree":
209
+ return tuple(index_object._iter_convert_to_object(index_object._cache))
210
+ return ()
211
+
212
+ def _set_cache_(self, attr: str) -> None:
213
+ if attr == "_cache":
214
+ # Set the data when we need it.
215
+ ostream = self.repo.odb.stream(self.binsha)
216
+ self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read())
217
+ else:
218
+ super()._set_cache_(attr)
219
+ # END handle attribute
220
+
221
+ def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
222
+ """Iterable yields tuples of (binsha, mode, name), which will be converted to
223
+ the respective object representation.
224
+ """
225
+ for binsha, mode, name in iterable:
226
+ path = join_path(self.path, name)
227
+ try:
228
+ yield self._map_id_to_type[mode >> 12](self.repo, binsha, mode, path)
229
+ except KeyError as e:
230
+ raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e
231
+ # END for each item
232
+
233
+ def join(self, file: str) -> IndexObjUnion:
234
+ """Find the named object in this tree's contents.
235
+
236
+ :return:
237
+ :class:`~git.objects.blob.Blob`, :class:`Tree`, or
238
+ :class:`~git.objects.submodule.base.Submodule`
239
+
240
+ :raise KeyError:
241
+ If the given file or tree does not exist in this tree.
242
+ """
243
+ msg = "Blob or Tree named %r not found"
244
+ if "/" in file:
245
+ tree = self
246
+ item = self
247
+ tokens = file.split("/")
248
+ for i, token in enumerate(tokens):
249
+ item = tree[token]
250
+ if item.type == "tree":
251
+ tree = item
252
+ else:
253
+ # Safety assertion - blobs are at the end of the path.
254
+ if i != len(tokens) - 1:
255
+ raise KeyError(msg % file)
256
+ return item
257
+ # END handle item type
258
+ # END for each token of split path
259
+ if item == self:
260
+ raise KeyError(msg % file)
261
+ return item
262
+ else:
263
+ for info in self._cache:
264
+ if info[2] == file: # [2] == name
265
+ return self._map_id_to_type[info[1] >> 12](
266
+ self.repo, info[0], info[1], join_path(self.path, info[2])
267
+ )
268
+ # END for each obj
269
+ raise KeyError(msg % file)
270
+ # END handle long paths
271
+
272
+ def __truediv__(self, file: str) -> IndexObjUnion:
273
+ """The ``/`` operator is another syntax for joining.
274
+
275
+ See :meth:`join` for details.
276
+ """
277
+ return self.join(file)
278
+
279
+ @property
280
+ def trees(self) -> List["Tree"]:
281
+ """:return: list(Tree, ...) List of trees directly below this tree"""
282
+ return [i for i in self if i.type == "tree"]
283
+
284
+ @property
285
+ def blobs(self) -> List[Blob]:
286
+ """:return: list(Blob, ...) List of blobs directly below this tree"""
287
+ return [i for i in self if i.type == "blob"]
288
+
289
+ @property
290
+ def cache(self) -> TreeModifier:
291
+ """
292
+ :return:
293
+ An object allowing modification of the internal cache. This can be used to
294
+ change the tree's contents. When done, make sure you call
295
+ :meth:`~TreeModifier.set_done` on the tree modifier, or serialization
296
+ behaviour will be incorrect.
297
+
298
+ :note:
299
+ See :class:`TreeModifier` for more information on how to alter the cache.
300
+ """
301
+ return TreeModifier(self._cache)
302
+
303
+ def traverse(
304
+ self,
305
+ predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True,
306
+ prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False,
307
+ depth: int = -1,
308
+ branch_first: bool = True,
309
+ visit_once: bool = False,
310
+ ignore_self: int = 1,
311
+ as_edge: bool = False,
312
+ ) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]:
313
+ """For documentation, see
314
+ `Traversable._traverse() <git.objects.util.Traversable._traverse>`.
315
+
316
+ Trees are set to ``visit_once = False`` to gain more performance in the
317
+ traversal.
318
+ """
319
+
320
+ # # To typecheck instead of using cast.
321
+ # import itertools
322
+ # def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]:
323
+ # return all(isinstance(x, (Blob, Tree, Submodule)) for x in inp[1])
324
+
325
+ # ret = super().traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
326
+ # ret_tup = itertools.tee(ret, 2)
327
+ # assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}"
328
+ # return ret_tup[0]
329
+
330
+ return cast(
331
+ Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
332
+ super()._traverse(
333
+ predicate, # type: ignore[arg-type]
334
+ prune, # type: ignore[arg-type]
335
+ depth,
336
+ branch_first,
337
+ visit_once,
338
+ ignore_self,
339
+ ),
340
+ )
341
+
342
+ def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion]:
343
+ """
344
+ :return:
345
+ :class:`~git.util.IterableList` with the results of the traversal as
346
+ produced by :meth:`traverse`
347
+
348
+ Tree -> IterableList[Union[Submodule, Tree, Blob]]
349
+ """
350
+ return super()._list_traverse(*args, **kwargs)
351
+
352
+ # List protocol
353
+
354
+ def __getslice__(self, i: int, j: int) -> List[IndexObjUnion]:
355
+ return list(self._iter_convert_to_object(self._cache[i:j]))
356
+
357
+ def __iter__(self) -> Iterator[IndexObjUnion]:
358
+ return self._iter_convert_to_object(self._cache)
359
+
360
+ def __len__(self) -> int:
361
+ return len(self._cache)
362
+
363
+ def __getitem__(self, item: Union[str, int, slice]) -> IndexObjUnion:
364
+ if isinstance(item, int):
365
+ info = self._cache[item]
366
+ return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2]))
367
+
368
+ if isinstance(item, str):
369
+ # compatibility
370
+ return self.join(item)
371
+ # END index is basestring
372
+
373
+ raise TypeError("Invalid index type: %r" % item)
374
+
375
+ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
376
+ if isinstance(item, IndexObject):
377
+ for info in self._cache:
378
+ if item.binsha == info[0]:
379
+ return True
380
+ # END compare sha
381
+ # END for each entry
382
+ # END handle item is index object
383
+ # compatibility
384
+
385
+ # Treat item as repo-relative path.
386
+ else:
387
+ path = self.path
388
+ for info in self._cache:
389
+ if item == join_path(path, info[2]):
390
+ return True
391
+ # END for each item
392
+ return False
393
+
394
+ def __reversed__(self) -> Iterator[IndexObjUnion]:
395
+ return reversed(self._iter_convert_to_object(self._cache)) # type: ignore[call-overload]
396
+
397
+ def _serialize(self, stream: "BytesIO") -> "Tree":
398
+ """Serialize this tree into the stream. Assumes sorted tree data.
399
+
400
+ :note:
401
+ We will assume our tree data to be in a sorted state. If this is not the
402
+ case, serialization will not generate a correct tree representation as these
403
+ are assumed to be sorted by algorithms.
404
+ """
405
+ tree_to_stream(self._cache, stream.write)
406
+ return self
407
+
408
+ def _deserialize(self, stream: "BytesIO") -> "Tree":
409
+ self._cache = tree_entries_from_data(stream.read())
410
+ return self
411
+
412
+
413
+ # END tree
414
+
415
+ # Finalize map definition.
416
+ Tree._map_id_to_type[Tree.tree_id] = Tree
.venv/lib/python3.11/site-packages/git/objects/util.py ADDED
@@ -0,0 +1,700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Utility functions for working with git objects."""
7
+
8
+ __all__ = [
9
+ "get_object_type_by_name",
10
+ "parse_date",
11
+ "parse_actor_and_date",
12
+ "ProcessStreamAdapter",
13
+ "Traversable",
14
+ "altz_to_utctz_str",
15
+ "utctz_to_altz",
16
+ "verify_utctz",
17
+ "Actor",
18
+ "tzoffset",
19
+ "utc",
20
+ ]
21
+
22
+ from abc import ABC, abstractmethod
23
+ import calendar
24
+ from collections import deque
25
+ from datetime import datetime, timedelta, tzinfo
26
+ import re
27
+ from string import digits
28
+ import time
29
+ import warnings
30
+
31
+ from git.util import Actor, IterableList, IterableObj
32
+
33
+ # typing ------------------------------------------------------------
34
+
35
+ from typing import (
36
+ Any,
37
+ Callable,
38
+ Deque,
39
+ Iterator,
40
+ NamedTuple,
41
+ Sequence,
42
+ TYPE_CHECKING,
43
+ Tuple,
44
+ Type,
45
+ TypeVar,
46
+ Union,
47
+ cast,
48
+ overload,
49
+ )
50
+
51
+ from git.types import Has_id_attribute, Literal
52
+
53
+ if TYPE_CHECKING:
54
+ from io import BytesIO, StringIO
55
+ from subprocess import Popen
56
+
57
+ from git.types import Protocol, runtime_checkable
58
+
59
+ from .blob import Blob
60
+ from .commit import Commit
61
+ from .submodule.base import Submodule
62
+ from .tag import TagObject
63
+ from .tree import TraversedTreeTup, Tree
64
+ else:
65
+ Protocol = ABC
66
+
67
+ def runtime_checkable(f):
68
+ return f
69
+
70
+
71
+ class TraverseNT(NamedTuple):
72
+ depth: int
73
+ item: Union["Traversable", "Blob"]
74
+ src: Union["Traversable", None]
75
+
76
+
77
+ T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj") # For TraversableIterableObj.traverse()
78
+
79
+ TraversedTup = Union[
80
+ Tuple[Union["Traversable", None], "Traversable"], # For Commit, Submodule.
81
+ "TraversedTreeTup", # For Tree.traverse().
82
+ ]
83
+
84
+ # --------------------------------------------------------------------
85
+
86
+ ZERO = timedelta(0)
87
+
88
+ # { Functions
89
+
90
+
91
+ def mode_str_to_int(modestr: Union[bytes, str]) -> int:
92
+ """Convert mode bits from an octal mode string to an integer mode for git.
93
+
94
+ :param modestr:
95
+ String like ``755`` or ``644`` or ``100644`` - only the last 6 chars will be
96
+ used.
97
+
98
+ :return:
99
+ String identifying a mode compatible to the mode methods ids of the :mod:`stat`
100
+ module regarding the rwx permissions for user, group and other, special flags
101
+ and file system flags, such as whether it is a symlink.
102
+ """
103
+ mode = 0
104
+ for iteration, char in enumerate(reversed(modestr[-6:])):
105
+ char = cast(Union[str, int], char)
106
+ mode += int(char) << iteration * 3
107
+ # END for each char
108
+ return mode
109
+
110
+
111
+ def get_object_type_by_name(
112
+ object_type_name: bytes,
113
+ ) -> Union[Type["Commit"], Type["TagObject"], Type["Tree"], Type["Blob"]]:
114
+ """Retrieve the Python class GitPython uses to represent a kind of Git object.
115
+
116
+ :return:
117
+ A type suitable to handle the given as `object_type_name`.
118
+ This type can be called create new instances.
119
+
120
+ :param object_type_name:
121
+ Member of :attr:`Object.TYPES <git.objects.base.Object.TYPES>`.
122
+
123
+ :raise ValueError:
124
+ If `object_type_name` is unknown.
125
+ """
126
+ if object_type_name == b"commit":
127
+ from . import commit
128
+
129
+ return commit.Commit
130
+ elif object_type_name == b"tag":
131
+ from . import tag
132
+
133
+ return tag.TagObject
134
+ elif object_type_name == b"blob":
135
+ from . import blob
136
+
137
+ return blob.Blob
138
+ elif object_type_name == b"tree":
139
+ from . import tree
140
+
141
+ return tree.Tree
142
+ else:
143
+ raise ValueError("Cannot handle unknown object type: %s" % object_type_name.decode())
144
+
145
+
146
+ def utctz_to_altz(utctz: str) -> int:
147
+ """Convert a git timezone offset into a timezone offset west of UTC in seconds
148
+ (compatible with :attr:`time.altzone`).
149
+
150
+ :param utctz:
151
+ git utc timezone string, e.g. +0200
152
+ """
153
+ int_utctz = int(utctz)
154
+ seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60
155
+ return seconds if int_utctz < 0 else -seconds
156
+
157
+
158
+ def altz_to_utctz_str(altz: float) -> str:
159
+ """Convert a timezone offset west of UTC in seconds into a Git timezone offset
160
+ string.
161
+
162
+ :param altz:
163
+ Timezone offset in seconds west of UTC.
164
+ """
165
+ hours = abs(altz) // 3600
166
+ minutes = (abs(altz) % 3600) // 60
167
+ sign = "-" if altz >= 60 else "+"
168
+ return "{}{:02}{:02}".format(sign, hours, minutes)
169
+
170
+
171
+ def verify_utctz(offset: str) -> str:
172
+ """
173
+ :raise ValueError:
174
+ If `offset` is incorrect.
175
+
176
+ :return:
177
+ `offset`
178
+ """
179
+ fmt_exc = ValueError("Invalid timezone offset format: %s" % offset)
180
+ if len(offset) != 5:
181
+ raise fmt_exc
182
+ if offset[0] not in "+-":
183
+ raise fmt_exc
184
+ if offset[1] not in digits or offset[2] not in digits or offset[3] not in digits or offset[4] not in digits:
185
+ raise fmt_exc
186
+ # END for each char
187
+ return offset
188
+
189
+
190
+ class tzoffset(tzinfo):
191
+ def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None:
192
+ self._offset = timedelta(seconds=-secs_west_of_utc)
193
+ self._name = name or "fixed"
194
+
195
+ def __reduce__(self) -> Tuple[Type["tzoffset"], Tuple[float, str]]:
196
+ return tzoffset, (-self._offset.total_seconds(), self._name)
197
+
198
+ def utcoffset(self, dt: Union[datetime, None]) -> timedelta:
199
+ return self._offset
200
+
201
+ def tzname(self, dt: Union[datetime, None]) -> str:
202
+ return self._name
203
+
204
+ def dst(self, dt: Union[datetime, None]) -> timedelta:
205
+ return ZERO
206
+
207
+
208
+ utc = tzoffset(0, "UTC")
209
+
210
+
211
+ def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
212
+ """Convert a `timestamp` + `tz_offset` into an aware :class:`~datetime.datetime`
213
+ instance."""
214
+ utc_dt = datetime.fromtimestamp(timestamp, utc)
215
+ try:
216
+ local_dt = utc_dt.astimezone(tzoffset(tz_offset))
217
+ return local_dt
218
+ except ValueError:
219
+ return utc_dt
220
+
221
+
222
+ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
223
+ """Parse the given date as one of the following:
224
+
225
+ * Aware datetime instance
226
+ * Git internal format: timestamp offset
227
+ * :rfc:`2822`: ``Thu, 07 Apr 2005 22:13:13 +0200``
228
+ * ISO 8601: ``2005-04-07T22:13:13`` - The ``T`` can be a space as well.
229
+
230
+ :return:
231
+ Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch
232
+
233
+ :raise ValueError:
234
+ If the format could not be understood.
235
+
236
+ :note:
237
+ Date can also be ``YYYY.MM.DD``, ``MM/DD/YYYY`` and ``DD.MM.YYYY``.
238
+ """
239
+ if isinstance(string_date, datetime):
240
+ if string_date.tzinfo:
241
+ utcoffset = cast(timedelta, string_date.utcoffset()) # typeguard, if tzinfoand is not None
242
+ offset = -int(utcoffset.total_seconds())
243
+ return int(string_date.astimezone(utc).timestamp()), offset
244
+ else:
245
+ raise ValueError(f"string_date datetime object without tzinfo, {string_date}")
246
+
247
+ # Git time
248
+ try:
249
+ if string_date.count(" ") == 1 and string_date.rfind(":") == -1:
250
+ timestamp, offset_str = string_date.split()
251
+ if timestamp.startswith("@"):
252
+ timestamp = timestamp[1:]
253
+ timestamp_int = int(timestamp)
254
+ return timestamp_int, utctz_to_altz(verify_utctz(offset_str))
255
+ else:
256
+ offset_str = "+0000" # Local time by default.
257
+ if string_date[-5] in "-+":
258
+ offset_str = verify_utctz(string_date[-5:])
259
+ string_date = string_date[:-6] # skip space as well
260
+ # END split timezone info
261
+ offset = utctz_to_altz(offset_str)
262
+
263
+ # Now figure out the date and time portion - split time.
264
+ date_formats = []
265
+ splitter = -1
266
+ if "," in string_date:
267
+ date_formats.append("%a, %d %b %Y")
268
+ splitter = string_date.rfind(" ")
269
+ else:
270
+ # ISO plus additional
271
+ date_formats.append("%Y-%m-%d")
272
+ date_formats.append("%Y.%m.%d")
273
+ date_formats.append("%m/%d/%Y")
274
+ date_formats.append("%d.%m.%Y")
275
+
276
+ splitter = string_date.rfind("T")
277
+ if splitter == -1:
278
+ splitter = string_date.rfind(" ")
279
+ # END handle 'T' and ' '
280
+ # END handle RFC or ISO
281
+
282
+ assert splitter > -1
283
+
284
+ # Split date and time.
285
+ time_part = string_date[splitter + 1 :] # Skip space.
286
+ date_part = string_date[:splitter]
287
+
288
+ # Parse time.
289
+ tstruct = time.strptime(time_part, "%H:%M:%S")
290
+
291
+ for fmt in date_formats:
292
+ try:
293
+ dtstruct = time.strptime(date_part, fmt)
294
+ utctime = calendar.timegm(
295
+ (
296
+ dtstruct.tm_year,
297
+ dtstruct.tm_mon,
298
+ dtstruct.tm_mday,
299
+ tstruct.tm_hour,
300
+ tstruct.tm_min,
301
+ tstruct.tm_sec,
302
+ dtstruct.tm_wday,
303
+ dtstruct.tm_yday,
304
+ tstruct.tm_isdst,
305
+ )
306
+ )
307
+ return int(utctime), offset
308
+ except ValueError:
309
+ continue
310
+ # END exception handling
311
+ # END for each fmt
312
+
313
+ # Still here ? fail.
314
+ raise ValueError("no format matched")
315
+ # END handle format
316
+ except Exception as e:
317
+ raise ValueError(f"Unsupported date format or type: {string_date}, type={type(string_date)}") from e
318
+ # END handle exceptions
319
+
320
+
321
+ # Precompiled regexes
322
+ _re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")
323
+ _re_only_actor = re.compile(r"^.+? (.*)$")
324
+
325
+
326
+ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
327
+ """Parse out the actor (author or committer) info from a line like::
328
+
329
+ author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
330
+
331
+ :return:
332
+ [Actor, int_seconds_since_epoch, int_timezone_offset]
333
+ """
334
+ actor, epoch, offset = "", "0", "0"
335
+ m = _re_actor_epoch.search(line)
336
+ if m:
337
+ actor, epoch, offset = m.groups()
338
+ else:
339
+ m = _re_only_actor.search(line)
340
+ actor = m.group(1) if m else line or ""
341
+ return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset))
342
+
343
+
344
+ # } END functions
345
+
346
+
347
+ # { Classes
348
+
349
+
350
+ class ProcessStreamAdapter:
351
+ """Class wiring all calls to the contained Process instance.
352
+
353
+ Use this type to hide the underlying process to provide access only to a specified
354
+ stream. The process is usually wrapped into an :class:`~git.cmd.Git.AutoInterrupt`
355
+ class to kill it if the instance goes out of scope.
356
+ """
357
+
358
+ __slots__ = ("_proc", "_stream")
359
+
360
+ def __init__(self, process: "Popen", stream_name: str) -> None:
361
+ self._proc = process
362
+ self._stream: StringIO = getattr(process, stream_name) # guessed type
363
+
364
+ def __getattr__(self, attr: str) -> Any:
365
+ return getattr(self._stream, attr)
366
+
367
+
368
+ @runtime_checkable
369
+ class Traversable(Protocol):
370
+ """Simple interface to perform depth-first or breadth-first traversals in one
371
+ direction.
372
+
373
+ Subclasses only need to implement one function.
374
+
375
+ Instances of the subclass must be hashable.
376
+
377
+ Defined subclasses:
378
+
379
+ * :class:`Commit <git.objects.Commit>`
380
+ * :class:`Tree <git.objects.tree.Tree>`
381
+ * :class:`Submodule <git.objects.submodule.base.Submodule>`
382
+ """
383
+
384
+ __slots__ = ()
385
+
386
+ @classmethod
387
+ @abstractmethod
388
+ def _get_intermediate_items(cls, item: Any) -> Sequence["Traversable"]:
389
+ """
390
+ :return:
391
+ Tuple of items connected to the given item.
392
+ Must be implemented in subclass.
393
+
394
+ class Commit:: (cls, Commit) -> Tuple[Commit, ...]
395
+ class Submodule:: (cls, Submodule) -> Iterablelist[Submodule]
396
+ class Tree:: (cls, Tree) -> Tuple[Tree, ...]
397
+ """
398
+ raise NotImplementedError("To be implemented in subclass")
399
+
400
+ @abstractmethod
401
+ def list_traverse(self, *args: Any, **kwargs: Any) -> Any:
402
+ """Traverse self and collect all items found.
403
+
404
+ Calling this directly on the abstract base class, including via a ``super()``
405
+ proxy, is deprecated. Only overridden implementations should be called.
406
+ """
407
+ warnings.warn(
408
+ "list_traverse() method should only be called from subclasses."
409
+ " Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
410
+ " The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
411
+ DeprecationWarning,
412
+ stacklevel=2,
413
+ )
414
+ return self._list_traverse(*args, **kwargs)
415
+
416
+ def _list_traverse(
417
+ self, as_edge: bool = False, *args: Any, **kwargs: Any
418
+ ) -> IterableList[Union["Commit", "Submodule", "Tree", "Blob"]]:
419
+ """Traverse self and collect all items found.
420
+
421
+ :return:
422
+ :class:`~git.util.IterableList` with the results of the traversal as
423
+ produced by :meth:`traverse`::
424
+
425
+ Commit -> IterableList[Commit]
426
+ Submodule -> IterableList[Submodule]
427
+ Tree -> IterableList[Union[Submodule, Tree, Blob]]
428
+ """
429
+ # Commit and Submodule have id.__attribute__ as IterableObj.
430
+ # Tree has id.__attribute__ inherited from IndexObject.
431
+ if isinstance(self, Has_id_attribute):
432
+ id = self._id_attribute_
433
+ else:
434
+ # Shouldn't reach here, unless Traversable subclass created with no
435
+ # _id_attribute_.
436
+ id = ""
437
+ # Could add _id_attribute_ to Traversable, or make all Traversable also
438
+ # Iterable?
439
+
440
+ if not as_edge:
441
+ out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
442
+ out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) # noqa: B026
443
+ return out
444
+ # Overloads in subclasses (mypy doesn't allow typing self: subclass).
445
+ # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]]
446
+ else:
447
+ # Raise DeprecationWarning, it doesn't make sense to use this.
448
+ out_list: IterableList = IterableList(self.traverse(*args, **kwargs))
449
+ return out_list
450
+
451
+ @abstractmethod
452
+ def traverse(self, *args: Any, **kwargs: Any) -> Any:
453
+ """Iterator yielding items found when traversing self.
454
+
455
+ Calling this directly on the abstract base class, including via a ``super()``
456
+ proxy, is deprecated. Only overridden implementations should be called.
457
+ """
458
+ warnings.warn(
459
+ "traverse() method should only be called from subclasses."
460
+ " Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
461
+ " The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
462
+ DeprecationWarning,
463
+ stacklevel=2,
464
+ )
465
+ return self._traverse(*args, **kwargs)
466
+
467
+ def _traverse(
468
+ self,
469
+ predicate: Callable[[Union["Traversable", "Blob", TraversedTup], int], bool] = lambda i, d: True,
470
+ prune: Callable[[Union["Traversable", "Blob", TraversedTup], int], bool] = lambda i, d: False,
471
+ depth: int = -1,
472
+ branch_first: bool = True,
473
+ visit_once: bool = True,
474
+ ignore_self: int = 1,
475
+ as_edge: bool = False,
476
+ ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
477
+ """Iterator yielding items found when traversing `self`.
478
+
479
+ :param predicate:
480
+ A function ``f(i,d)`` that returns ``False`` if item i at depth ``d`` should
481
+ not be included in the result.
482
+
483
+ :param prune:
484
+ A function ``f(i,d)`` that returns ``True`` if the search should stop at
485
+ item ``i`` at depth ``d``. Item ``i`` will not be returned.
486
+
487
+ :param depth:
488
+ Defines at which level the iteration should not go deeper if -1. There is no
489
+ limit if 0, you would effectively only get `self`, the root of the
490
+ iteration. If 1, you would only get the first level of
491
+ predecessors/successors.
492
+
493
+ :param branch_first:
494
+ If ``True``, items will be returned branch first, otherwise depth first.
495
+
496
+ :param visit_once:
497
+ If ``True``, items will only be returned once, although they might be
498
+ encountered several times. Loops are prevented that way.
499
+
500
+ :param ignore_self:
501
+ If ``True``, `self` will be ignored and automatically pruned from the
502
+ result. Otherwise it will be the first item to be returned. If `as_edge` is
503
+ ``True``, the source of the first edge is ``None``.
504
+
505
+ :param as_edge:
506
+ If ``True``, return a pair of items, first being the source, second the
507
+ destination, i.e. tuple(src, dest) with the edge spanning from source to
508
+ destination.
509
+
510
+ :return:
511
+ Iterator yielding items found when traversing `self`::
512
+
513
+ Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] Submodule ->
514
+ Iterator[Submodule, Tuple[Submodule, Submodule]] Tree ->
515
+ Iterator[Union[Blob, Tree, Submodule,
516
+ Tuple[Union[Submodule, Tree], Union[Blob, Tree,
517
+ Submodule]]]
518
+
519
+ ignore_self=True is_edge=True -> Iterator[item] ignore_self=True
520
+ is_edge=False --> Iterator[item] ignore_self=False is_edge=True ->
521
+ Iterator[item] | Iterator[Tuple[src, item]] ignore_self=False
522
+ is_edge=False -> Iterator[Tuple[src, item]]
523
+ """
524
+
525
+ visited = set()
526
+ stack: Deque[TraverseNT] = deque()
527
+ stack.append(TraverseNT(0, self, None)) # self is always depth level 0.
528
+
529
+ def addToStack(
530
+ stack: Deque[TraverseNT],
531
+ src_item: "Traversable",
532
+ branch_first: bool,
533
+ depth: int,
534
+ ) -> None:
535
+ lst = self._get_intermediate_items(item)
536
+ if not lst: # Empty list
537
+ return
538
+ if branch_first:
539
+ stack.extendleft(TraverseNT(depth, i, src_item) for i in lst)
540
+ else:
541
+ reviter = (TraverseNT(depth, lst[i], src_item) for i in range(len(lst) - 1, -1, -1))
542
+ stack.extend(reviter)
543
+
544
+ # END addToStack local method
545
+
546
+ while stack:
547
+ d, item, src = stack.pop() # Depth of item, item, item_source
548
+
549
+ if visit_once and item in visited:
550
+ continue
551
+
552
+ if visit_once:
553
+ visited.add(item)
554
+
555
+ rval: Union[TraversedTup, "Traversable", "Blob"]
556
+ if as_edge:
557
+ # If as_edge return (src, item) unless rrc is None
558
+ # (e.g. for first item).
559
+ rval = (src, item)
560
+ else:
561
+ rval = item
562
+
563
+ if prune(rval, d):
564
+ continue
565
+
566
+ skipStartItem = ignore_self and (item is self)
567
+ if not skipStartItem and predicate(rval, d):
568
+ yield rval
569
+
570
+ # Only continue to next level if this is appropriate!
571
+ next_d = d + 1
572
+ if depth > -1 and next_d > depth:
573
+ continue
574
+
575
+ addToStack(stack, item, branch_first, next_d)
576
+ # END for each item on work stack
577
+
578
+
579
+ @runtime_checkable
580
+ class Serializable(Protocol):
581
+ """Defines methods to serialize and deserialize objects from and into a data
582
+ stream."""
583
+
584
+ __slots__ = ()
585
+
586
+ # @abstractmethod
587
+ def _serialize(self, stream: "BytesIO") -> "Serializable":
588
+ """Serialize the data of this object into the given data stream.
589
+
590
+ :note:
591
+ A serialized object would :meth:`_deserialize` into the same object.
592
+
593
+ :param stream:
594
+ A file-like object.
595
+
596
+ :return:
597
+ self
598
+ """
599
+ raise NotImplementedError("To be implemented in subclass")
600
+
601
+ # @abstractmethod
602
+ def _deserialize(self, stream: "BytesIO") -> "Serializable":
603
+ """Deserialize all information regarding this object from the stream.
604
+
605
+ :param stream:
606
+ A file-like object.
607
+
608
+ :return:
609
+ self
610
+ """
611
+ raise NotImplementedError("To be implemented in subclass")
612
+
613
+
614
+ class TraversableIterableObj(IterableObj, Traversable):
615
+ __slots__ = ()
616
+
617
+ TIobj_tuple = Tuple[Union[T_TIobj, None], T_TIobj]
618
+
619
+ def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]:
620
+ return super()._list_traverse(*args, **kwargs)
621
+
622
+ @overload
623
+ def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ...
624
+
625
+ @overload
626
+ def traverse(
627
+ self: T_TIobj,
628
+ predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
629
+ prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
630
+ depth: int,
631
+ branch_first: bool,
632
+ visit_once: bool,
633
+ ignore_self: Literal[True],
634
+ as_edge: Literal[False],
635
+ ) -> Iterator[T_TIobj]: ...
636
+
637
+ @overload
638
+ def traverse(
639
+ self: T_TIobj,
640
+ predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
641
+ prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
642
+ depth: int,
643
+ branch_first: bool,
644
+ visit_once: bool,
645
+ ignore_self: Literal[False],
646
+ as_edge: Literal[True],
647
+ ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: ...
648
+
649
+ @overload
650
+ def traverse(
651
+ self: T_TIobj,
652
+ predicate: Callable[[Union[T_TIobj, TIobj_tuple], int], bool],
653
+ prune: Callable[[Union[T_TIobj, TIobj_tuple], int], bool],
654
+ depth: int,
655
+ branch_first: bool,
656
+ visit_once: bool,
657
+ ignore_self: Literal[True],
658
+ as_edge: Literal[True],
659
+ ) -> Iterator[Tuple[T_TIobj, T_TIobj]]: ...
660
+
661
+ def traverse(
662
+ self: T_TIobj,
663
+ predicate: Callable[[Union[T_TIobj, TIobj_tuple], int], bool] = lambda i, d: True,
664
+ prune: Callable[[Union[T_TIobj, TIobj_tuple], int], bool] = lambda i, d: False,
665
+ depth: int = -1,
666
+ branch_first: bool = True,
667
+ visit_once: bool = True,
668
+ ignore_self: int = 1,
669
+ as_edge: bool = False,
670
+ ) -> Union[Iterator[T_TIobj], Iterator[Tuple[T_TIobj, T_TIobj]], Iterator[TIobj_tuple]]:
671
+ """For documentation, see :meth:`Traversable._traverse`."""
672
+
673
+ ## To typecheck instead of using cast:
674
+ #
675
+ # import itertools
676
+ # from git.types import TypeGuard
677
+ # def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit', 'Commit']]]]:
678
+ # for x in inp[1]:
679
+ # if not isinstance(x, tuple) and len(x) != 2:
680
+ # if all(isinstance(inner, Commit) for inner in x):
681
+ # continue
682
+ # return True
683
+ #
684
+ # ret = super(Commit, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge)
685
+ # ret_tup = itertools.tee(ret, 2)
686
+ # assert is_commit_traversed(ret_tup), f"{[type(x) for x in list(ret_tup[0])]}"
687
+ # return ret_tup[0]
688
+
689
+ return cast(
690
+ Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
691
+ super()._traverse(
692
+ predicate, # type: ignore[arg-type]
693
+ prune, # type: ignore[arg-type]
694
+ depth,
695
+ branch_first,
696
+ visit_once,
697
+ ignore_self,
698
+ as_edge,
699
+ ),
700
+ )
.venv/lib/python3.11/site-packages/git/py.typed ADDED
File without changes
.venv/lib/python3.11/site-packages/git/refs/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = [
5
+ "HEAD",
6
+ "Head",
7
+ "RefLog",
8
+ "RefLogEntry",
9
+ "Reference",
10
+ "RemoteReference",
11
+ "SymbolicReference",
12
+ "Tag",
13
+ "TagReference",
14
+ ]
15
+
16
+ from .head import HEAD, Head
17
+ from .log import RefLog, RefLogEntry
18
+ from .reference import Reference
19
+ from .remote import RemoteReference
20
+ from .symbolic import SymbolicReference
21
+ from .tag import Tag, TagReference
.venv/lib/python3.11/site-packages/git/refs/head.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Some ref-based objects.
5
+
6
+ Note the distinction between the :class:`HEAD` and :class:`Head` classes.
7
+ """
8
+
9
+ __all__ = ["HEAD", "Head"]
10
+
11
+ from git.config import GitConfigParser, SectionConstraint
12
+ from git.exc import GitCommandError
13
+ from git.util import join_path
14
+
15
+ from .reference import Reference
16
+ from .symbolic import SymbolicReference
17
+
18
+ # typing ---------------------------------------------------
19
+
20
+ from typing import Any, Sequence, TYPE_CHECKING, Union
21
+
22
+ from git.types import Commit_ish, PathLike
23
+
24
+ if TYPE_CHECKING:
25
+ from git.objects import Commit
26
+ from git.refs import RemoteReference
27
+ from git.repo import Repo
28
+
29
+ # -------------------------------------------------------------------
30
+
31
+
32
+ def strip_quotes(string: str) -> str:
33
+ if string.startswith('"') and string.endswith('"'):
34
+ return string[1:-1]
35
+ return string
36
+
37
+
38
+ class HEAD(SymbolicReference):
39
+ """Special case of a :class:`~git.refs.symbolic.SymbolicReference` representing the
40
+ repository's HEAD reference."""
41
+
42
+ _HEAD_NAME = "HEAD"
43
+ _ORIG_HEAD_NAME = "ORIG_HEAD"
44
+
45
+ __slots__ = ()
46
+
47
+ # TODO: This can be removed once SymbolicReference.commit has static type hints.
48
+ commit: "Commit"
49
+
50
+ def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None:
51
+ if path != self._HEAD_NAME:
52
+ raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path))
53
+ super().__init__(repo, path)
54
+
55
+ def orig_head(self) -> SymbolicReference:
56
+ """
57
+ :return:
58
+ :class:`~git.refs.symbolic.SymbolicReference` pointing at the ORIG_HEAD,
59
+ which is maintained to contain the previous value of HEAD.
60
+ """
61
+ return SymbolicReference(self.repo, self._ORIG_HEAD_NAME)
62
+
63
+ def reset(
64
+ self,
65
+ commit: Union[Commit_ish, SymbolicReference, str] = "HEAD",
66
+ index: bool = True,
67
+ working_tree: bool = False,
68
+ paths: Union[PathLike, Sequence[PathLike], None] = None,
69
+ **kwargs: Any,
70
+ ) -> "HEAD":
71
+ """Reset our HEAD to the given commit optionally synchronizing the index and
72
+ working tree. The reference we refer to will be set to commit as well.
73
+
74
+ :param commit:
75
+ :class:`~git.objects.commit.Commit`, :class:`~git.refs.reference.Reference`,
76
+ or string identifying a revision we should reset HEAD to.
77
+
78
+ :param index:
79
+ If ``True``, the index will be set to match the given commit.
80
+ Otherwise it will not be touched.
81
+
82
+ :param working_tree:
83
+ If ``True``, the working tree will be forcefully adjusted to match the given
84
+ commit, possibly overwriting uncommitted changes without warning.
85
+ If `working_tree` is ``True``, `index` must be ``True`` as well.
86
+
87
+ :param paths:
88
+ Single path or list of paths relative to the git root directory
89
+ that are to be reset. This allows to partially reset individual files.
90
+
91
+ :param kwargs:
92
+ Additional arguments passed to :manpage:`git-reset(1)`.
93
+
94
+ :return:
95
+ self
96
+ """
97
+ mode: Union[str, None]
98
+ mode = "--soft"
99
+ if index:
100
+ mode = "--mixed"
101
+
102
+ # Explicit "--mixed" when passing paths is deprecated since git 1.5.4.
103
+ # See https://github.com/gitpython-developers/GitPython/discussions/1876.
104
+ if paths:
105
+ mode = None
106
+ # END special case
107
+ # END handle index
108
+
109
+ if working_tree:
110
+ mode = "--hard"
111
+ if not index:
112
+ raise ValueError("Cannot reset the working tree if the index is not reset as well")
113
+
114
+ # END working tree handling
115
+
116
+ try:
117
+ self.repo.git.reset(mode, commit, "--", paths, **kwargs)
118
+ except GitCommandError as e:
119
+ # git nowadays may use 1 as status to indicate there are still unstaged
120
+ # modifications after the reset.
121
+ if e.status != 1:
122
+ raise
123
+ # END handle exception
124
+
125
+ return self
126
+
127
+
128
+ class Head(Reference):
129
+ """A Head is a named reference to a :class:`~git.objects.commit.Commit`. Every Head
130
+ instance contains a name and a :class:`~git.objects.commit.Commit` object.
131
+
132
+ Examples::
133
+
134
+ >>> repo = Repo("/path/to/repo")
135
+ >>> head = repo.heads[0]
136
+
137
+ >>> head.name
138
+ 'master'
139
+
140
+ >>> head.commit
141
+ <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
142
+
143
+ >>> head.commit.hexsha
144
+ '1c09f116cbc2cb4100fb6935bb162daa4723f455'
145
+ """
146
+
147
+ _common_path_default = "refs/heads"
148
+ k_config_remote = "remote"
149
+ k_config_remote_ref = "merge" # Branch to merge from remote.
150
+
151
+ @classmethod
152
+ def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None:
153
+ """Delete the given heads.
154
+
155
+ :param force:
156
+ If ``True``, the heads will be deleted even if they are not yet merged into
157
+ the main development stream. Default ``False``.
158
+ """
159
+ flag = "-d"
160
+ if force:
161
+ flag = "-D"
162
+ repo.git.branch(flag, *heads)
163
+
164
+ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None]) -> "Head":
165
+ """Configure this branch to track the given remote reference. This will
166
+ alter this branch's configuration accordingly.
167
+
168
+ :param remote_reference:
169
+ The remote reference to track or None to untrack any references.
170
+
171
+ :return:
172
+ self
173
+ """
174
+ from .remote import RemoteReference
175
+
176
+ if remote_reference is not None and not isinstance(remote_reference, RemoteReference):
177
+ raise ValueError("Incorrect parameter type: %r" % remote_reference)
178
+ # END handle type
179
+
180
+ with self.config_writer() as writer:
181
+ if remote_reference is None:
182
+ writer.remove_option(self.k_config_remote)
183
+ writer.remove_option(self.k_config_remote_ref)
184
+ if len(writer.options()) == 0:
185
+ writer.remove_section()
186
+ else:
187
+ writer.set_value(self.k_config_remote, remote_reference.remote_name)
188
+ writer.set_value(
189
+ self.k_config_remote_ref,
190
+ Head.to_full_path(remote_reference.remote_head),
191
+ )
192
+
193
+ return self
194
+
195
+ def tracking_branch(self) -> Union["RemoteReference", None]:
196
+ """
197
+ :return:
198
+ The remote reference we are tracking, or ``None`` if we are not a tracking
199
+ branch.
200
+ """
201
+ from .remote import RemoteReference
202
+
203
+ reader = self.config_reader()
204
+ if reader.has_option(self.k_config_remote) and reader.has_option(self.k_config_remote_ref):
205
+ ref = Head(
206
+ self.repo,
207
+ Head.to_full_path(strip_quotes(reader.get_value(self.k_config_remote_ref))),
208
+ )
209
+ remote_refpath = RemoteReference.to_full_path(join_path(reader.get_value(self.k_config_remote), ref.name))
210
+ return RemoteReference(self.repo, remote_refpath)
211
+ # END handle have tracking branch
212
+
213
+ # We are not a tracking branch.
214
+ return None
215
+
216
+ def rename(self, new_path: PathLike, force: bool = False) -> "Head":
217
+ """Rename self to a new path.
218
+
219
+ :param new_path:
220
+ Either a simple name or a path, e.g. ``new_name`` or ``features/new_name``.
221
+ The prefix ``refs/heads`` is implied.
222
+
223
+ :param force:
224
+ If ``True``, the rename will succeed even if a head with the target name
225
+ already exists.
226
+
227
+ :return:
228
+ self
229
+
230
+ :note:
231
+ Respects the ref log, as git commands are used.
232
+ """
233
+ flag = "-m"
234
+ if force:
235
+ flag = "-M"
236
+
237
+ self.repo.git.branch(flag, self, new_path)
238
+ self.path = "%s/%s" % (self._common_path_default, new_path)
239
+ return self
240
+
241
+ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
242
+ """Check out this head by setting the HEAD to this reference, by updating the
243
+ index to reflect the tree we point to and by updating the working tree to
244
+ reflect the latest index.
245
+
246
+ The command will fail if changed working tree files would be overwritten.
247
+
248
+ :param force:
249
+ If ``True``, changes to the index and the working tree will be discarded.
250
+ If ``False``, :exc:`~git.exc.GitCommandError` will be raised in that
251
+ situation.
252
+
253
+ :param kwargs:
254
+ Additional keyword arguments to be passed to git checkout, e.g.
255
+ ``b="new_branch"`` to create a new branch at the given spot.
256
+
257
+ :return:
258
+ The active branch after the checkout operation, usually self unless a new
259
+ branch has been created.
260
+ If there is no active branch, as the HEAD is now detached, the HEAD
261
+ reference will be returned instead.
262
+
263
+ :note:
264
+ By default it is only allowed to checkout heads - everything else will leave
265
+ the HEAD detached which is allowed and possible, but remains a special state
266
+ that some tools might not be able to handle.
267
+ """
268
+ kwargs["f"] = force
269
+ if kwargs["f"] is False:
270
+ kwargs.pop("f")
271
+
272
+ self.repo.git.checkout(self, **kwargs)
273
+ if self.repo.head.is_detached:
274
+ return self.repo.head
275
+ else:
276
+ return self.repo.active_branch
277
+
278
+ # { Configuration
279
+ def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]:
280
+ if read_only:
281
+ parser = self.repo.config_reader()
282
+ else:
283
+ parser = self.repo.config_writer()
284
+ # END handle parser instance
285
+
286
+ return SectionConstraint(parser, 'branch "%s"' % self.name)
287
+
288
+ def config_reader(self) -> SectionConstraint[GitConfigParser]:
289
+ """
290
+ :return:
291
+ A configuration parser instance constrained to only read this instance's
292
+ values.
293
+ """
294
+ return self._config_parser(read_only=True)
295
+
296
+ def config_writer(self) -> SectionConstraint[GitConfigParser]:
297
+ """
298
+ :return:
299
+ A configuration writer instance with read-and write access to options of
300
+ this head.
301
+ """
302
+ return self._config_parser(read_only=False)
303
+
304
+ # } END configuration
.venv/lib/python3.11/site-packages/git/refs/log.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["RefLog", "RefLogEntry"]
5
+
6
+ from mmap import mmap
7
+ import os.path as osp
8
+ import re
9
+ import time as _time
10
+
11
+ from git.compat import defenc
12
+ from git.objects.util import (
13
+ Serializable,
14
+ altz_to_utctz_str,
15
+ parse_date,
16
+ )
17
+ from git.util import (
18
+ Actor,
19
+ LockedFD,
20
+ LockFile,
21
+ assure_directory_exists,
22
+ bin_to_hex,
23
+ file_contents_ro_filepath,
24
+ to_native_path,
25
+ )
26
+
27
+ # typing ------------------------------------------------------------------
28
+
29
+ from typing import Iterator, List, Tuple, TYPE_CHECKING, Union
30
+
31
+ from git.types import PathLike
32
+
33
+ if TYPE_CHECKING:
34
+ from io import BytesIO
35
+
36
+ from git.config import GitConfigParser, SectionConstraint
37
+ from git.refs import SymbolicReference
38
+
39
+ # ------------------------------------------------------------------------------
40
+
41
+
42
+ class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
43
+ """Named tuple allowing easy access to the revlog data fields."""
44
+
45
+ _re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
46
+
47
+ __slots__ = ()
48
+
49
+ def __repr__(self) -> str:
50
+ """Representation of ourselves in git reflog format."""
51
+ return self.format()
52
+
53
+ def format(self) -> str:
54
+ """:return: A string suitable to be placed in a reflog file."""
55
+ act = self.actor
56
+ time = self.time
57
+ return "{} {} {} <{}> {!s} {}\t{}\n".format(
58
+ self.oldhexsha,
59
+ self.newhexsha,
60
+ act.name,
61
+ act.email,
62
+ time[0],
63
+ altz_to_utctz_str(time[1]),
64
+ self.message,
65
+ )
66
+
67
+ @property
68
+ def oldhexsha(self) -> str:
69
+ """The hexsha to the commit the ref pointed to before the change."""
70
+ return self[0]
71
+
72
+ @property
73
+ def newhexsha(self) -> str:
74
+ """The hexsha to the commit the ref now points to, after the change."""
75
+ return self[1]
76
+
77
+ @property
78
+ def actor(self) -> Actor:
79
+ """Actor instance, providing access."""
80
+ return self[2]
81
+
82
+ @property
83
+ def time(self) -> Tuple[int, int]:
84
+ """Time as tuple:
85
+
86
+ * [0] = ``int(time)``
87
+ * [1] = ``int(timezone_offset)`` in :attr:`time.altzone` format
88
+ """
89
+ return self[3]
90
+
91
+ @property
92
+ def message(self) -> str:
93
+ """Message describing the operation that acted on the reference."""
94
+ return self[4]
95
+
96
+ @classmethod
97
+ def new(
98
+ cls,
99
+ oldhexsha: str,
100
+ newhexsha: str,
101
+ actor: Actor,
102
+ time: int,
103
+ tz_offset: int,
104
+ message: str,
105
+ ) -> "RefLogEntry": # skipcq: PYL-W0621
106
+ """:return: New instance of a :class:`RefLogEntry`"""
107
+ if not isinstance(actor, Actor):
108
+ raise ValueError("Need actor instance, got %s" % actor)
109
+ # END check types
110
+ return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message))
111
+
112
+ @classmethod
113
+ def from_line(cls, line: bytes) -> "RefLogEntry":
114
+ """:return: New :class:`RefLogEntry` instance from the given revlog line.
115
+
116
+ :param line:
117
+ Line bytes without trailing newline
118
+
119
+ :raise ValueError:
120
+ If `line` could not be parsed.
121
+ """
122
+ line_str = line.decode(defenc)
123
+ fields = line_str.split("\t", 1)
124
+ if len(fields) == 1:
125
+ info, msg = fields[0], None
126
+ elif len(fields) == 2:
127
+ info, msg = fields
128
+ else:
129
+ raise ValueError("Line must have up to two TAB-separated fields. Got %s" % repr(line_str))
130
+ # END handle first split
131
+
132
+ oldhexsha = info[:40]
133
+ newhexsha = info[41:81]
134
+ for hexsha in (oldhexsha, newhexsha):
135
+ if not cls._re_hexsha_only.match(hexsha):
136
+ raise ValueError("Invalid hexsha: %r" % (hexsha,))
137
+ # END if hexsha re doesn't match
138
+ # END for each hexsha
139
+
140
+ email_end = info.find(">", 82)
141
+ if email_end == -1:
142
+ raise ValueError("Missing token: >")
143
+ # END handle missing end brace
144
+
145
+ actor = Actor._from_string(info[82 : email_end + 1])
146
+ time, tz_offset = parse_date(info[email_end + 2 :]) # skipcq: PYL-W0621
147
+
148
+ return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg))
149
+
150
+
151
+ class RefLog(List[RefLogEntry], Serializable):
152
+ R"""A reflog contains :class:`RefLogEntry`\s, each of which defines a certain state
153
+ of the head in question. Custom query methods allow to retrieve log entries by date
154
+ or by other criteria.
155
+
156
+ Reflog entries are ordered. The first added entry is first in the list. The last
157
+ entry, i.e. the last change of the head or reference, is last in the list.
158
+ """
159
+
160
+ __slots__ = ("_path",)
161
+
162
+ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog":
163
+ inst = super().__new__(cls)
164
+ return inst
165
+
166
+ def __init__(self, filepath: Union[PathLike, None] = None) -> None:
167
+ """Initialize this instance with an optional filepath, from which we will
168
+ initialize our data. The path is also used to write changes back using the
169
+ :meth:`write` method."""
170
+ self._path = filepath
171
+ if filepath is not None:
172
+ self._read_from_file()
173
+ # END handle filepath
174
+
175
+ def _read_from_file(self) -> None:
176
+ try:
177
+ fmap = file_contents_ro_filepath(self._path, stream=True, allow_mmap=True)
178
+ except OSError:
179
+ # It is possible and allowed that the file doesn't exist!
180
+ return
181
+ # END handle invalid log
182
+
183
+ try:
184
+ self._deserialize(fmap)
185
+ finally:
186
+ fmap.close()
187
+ # END handle closing of handle
188
+
189
+ # { Interface
190
+
191
+ @classmethod
192
+ def from_file(cls, filepath: PathLike) -> "RefLog":
193
+ """
194
+ :return:
195
+ A new :class:`RefLog` instance containing all entries from the reflog at the
196
+ given `filepath`.
197
+
198
+ :param filepath:
199
+ Path to reflog.
200
+
201
+ :raise ValueError:
202
+ If the file could not be read or was corrupted in some way.
203
+ """
204
+ return cls(filepath)
205
+
206
+ @classmethod
207
+ def path(cls, ref: "SymbolicReference") -> str:
208
+ """
209
+ :return:
210
+ String to absolute path at which the reflog of the given ref instance would
211
+ be found. The path is not guaranteed to point to a valid file though.
212
+
213
+ :param ref:
214
+ :class:`~git.refs.symbolic.SymbolicReference` instance
215
+ """
216
+ return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path))
217
+
218
+ @classmethod
219
+ def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEntry]:
220
+ """
221
+ :return:
222
+ Iterator yielding :class:`RefLogEntry` instances, one for each line read
223
+ from the given stream.
224
+
225
+ :param stream:
226
+ File-like object containing the revlog in its native format or string
227
+ instance pointing to a file to read.
228
+ """
229
+ new_entry = RefLogEntry.from_line
230
+ if isinstance(stream, str):
231
+ # Default args return mmap since Python 3.
232
+ _stream = file_contents_ro_filepath(stream)
233
+ assert isinstance(_stream, mmap)
234
+ else:
235
+ _stream = stream
236
+ # END handle stream type
237
+ while True:
238
+ line = _stream.readline()
239
+ if not line:
240
+ return
241
+ yield new_entry(line.strip())
242
+ # END endless loop
243
+
244
+ @classmethod
245
+ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry":
246
+ """
247
+ :return:
248
+ :class:`RefLogEntry` at the given index.
249
+
250
+ :param filepath:
251
+ Full path to the index file from which to read the entry.
252
+
253
+ :param index:
254
+ Python list compatible index, i.e. it may be negative to specify an entry
255
+ counted from the end of the list.
256
+
257
+ :raise IndexError:
258
+ If the entry didn't exist.
259
+
260
+ :note:
261
+ This method is faster as it only parses the entry at index, skipping all
262
+ other lines. Nonetheless, the whole file has to be read if the index is
263
+ negative.
264
+ """
265
+ with open(filepath, "rb") as fp:
266
+ if index < 0:
267
+ return RefLogEntry.from_line(fp.readlines()[index].strip())
268
+ # Read until index is reached.
269
+
270
+ for i in range(index + 1):
271
+ line = fp.readline()
272
+ if not line:
273
+ raise IndexError(f"Index file ended at line {i + 1}, before given index was reached")
274
+ # END abort on eof
275
+ # END handle runup
276
+
277
+ return RefLogEntry.from_line(line.strip())
278
+ # END handle index
279
+
280
+ def to_file(self, filepath: PathLike) -> None:
281
+ """Write the contents of the reflog instance to a file at the given filepath.
282
+
283
+ :param filepath:
284
+ Path to file. Parent directories are assumed to exist.
285
+ """
286
+ lfd = LockedFD(filepath)
287
+ assure_directory_exists(filepath, is_file=True)
288
+
289
+ fp = lfd.open(write=True, stream=True)
290
+ try:
291
+ self._serialize(fp)
292
+ lfd.commit()
293
+ except BaseException:
294
+ lfd.rollback()
295
+ raise
296
+ # END handle change
297
+
298
+ @classmethod
299
+ def append_entry(
300
+ cls,
301
+ config_reader: Union[Actor, "GitConfigParser", "SectionConstraint", None],
302
+ filepath: PathLike,
303
+ oldbinsha: bytes,
304
+ newbinsha: bytes,
305
+ message: str,
306
+ write: bool = True,
307
+ ) -> "RefLogEntry":
308
+ """Append a new log entry to the revlog at filepath.
309
+
310
+ :param config_reader:
311
+ Configuration reader of the repository - used to obtain user information.
312
+ May also be an :class:`~git.util.Actor` instance identifying the committer
313
+ directly or ``None``.
314
+
315
+ :param filepath:
316
+ Full path to the log file.
317
+
318
+ :param oldbinsha:
319
+ Binary sha of the previous commit.
320
+
321
+ :param newbinsha:
322
+ Binary sha of the current commit.
323
+
324
+ :param message:
325
+ Message describing the change to the reference.
326
+
327
+ :param write:
328
+ If ``True``, the changes will be written right away.
329
+ Otherwise the change will not be written.
330
+
331
+ :return:
332
+ :class:`RefLogEntry` objects which was appended to the log.
333
+
334
+ :note:
335
+ As we are append-only, concurrent access is not a problem as we do not
336
+ interfere with readers.
337
+ """
338
+
339
+ if len(oldbinsha) != 20 or len(newbinsha) != 20:
340
+ raise ValueError("Shas need to be given in binary format")
341
+ # END handle sha type
342
+ assure_directory_exists(filepath, is_file=True)
343
+ first_line = message.split("\n")[0]
344
+ if isinstance(config_reader, Actor):
345
+ committer = config_reader # mypy thinks this is Actor | Gitconfigparser, but why?
346
+ else:
347
+ committer = Actor.committer(config_reader)
348
+ entry = RefLogEntry(
349
+ (
350
+ bin_to_hex(oldbinsha).decode("ascii"),
351
+ bin_to_hex(newbinsha).decode("ascii"),
352
+ committer,
353
+ (int(_time.time()), _time.altzone),
354
+ first_line,
355
+ )
356
+ )
357
+
358
+ if write:
359
+ lf = LockFile(filepath)
360
+ lf._obtain_lock_or_raise()
361
+ fd = open(filepath, "ab")
362
+ try:
363
+ fd.write(entry.format().encode(defenc))
364
+ finally:
365
+ fd.close()
366
+ lf._release_lock()
367
+ # END handle write operation
368
+ return entry
369
+
370
+ def write(self) -> "RefLog":
371
+ """Write this instance's data to the file we are originating from.
372
+
373
+ :return:
374
+ self
375
+ """
376
+ if self._path is None:
377
+ raise ValueError("Instance was not initialized with a path, use to_file(...) instead")
378
+ # END assert path
379
+ self.to_file(self._path)
380
+ return self
381
+
382
+ # } END interface
383
+
384
+ # { Serializable Interface
385
+
386
+ def _serialize(self, stream: "BytesIO") -> "RefLog":
387
+ write = stream.write
388
+
389
+ # Write all entries.
390
+ for e in self:
391
+ write(e.format().encode(defenc))
392
+ # END for each entry
393
+ return self
394
+
395
+ def _deserialize(self, stream: "BytesIO") -> "RefLog":
396
+ self.extend(self.iter_entries(stream))
397
+ return self
398
+
399
+ # } END serializable interface
.venv/lib/python3.11/site-packages/git/refs/reference.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["Reference"]
5
+
6
+ from git.util import IterableObj, LazyMixin
7
+
8
+ from .symbolic import SymbolicReference, T_References
9
+
10
+ # typing ------------------------------------------------------------------
11
+
12
+ from typing import Any, Callable, Iterator, TYPE_CHECKING, Type, Union
13
+
14
+ from git.types import AnyGitObject, PathLike, _T
15
+
16
+ if TYPE_CHECKING:
17
+ from git.repo import Repo
18
+
19
+ # ------------------------------------------------------------------------------
20
+
21
+ # { Utilities
22
+
23
+
24
+ def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
25
+ """A decorator raising :exc:`ValueError` if we are not a valid remote, based on the
26
+ path."""
27
+
28
+ def wrapper(self: T_References, *args: Any) -> _T:
29
+ if not self.is_remote():
30
+ raise ValueError("ref path does not point to a remote reference: %s" % self.path)
31
+ return func(self, *args)
32
+
33
+ # END wrapper
34
+ wrapper.__name__ = func.__name__
35
+ return wrapper
36
+
37
+
38
+ # } END utilities
39
+
40
+
41
+ class Reference(SymbolicReference, LazyMixin, IterableObj):
42
+ """A named reference to any object.
43
+
44
+ Subclasses may apply restrictions though, e.g., a :class:`~git.refs.head.Head` can
45
+ only point to commits.
46
+ """
47
+
48
+ __slots__ = ()
49
+
50
+ _points_to_commits_only = False
51
+ _resolve_ref_on_create = True
52
+ _common_path_default = "refs"
53
+
54
+ def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> None:
55
+ """Initialize this instance.
56
+
57
+ :param repo:
58
+ Our parent repository.
59
+
60
+ :param path:
61
+ Path relative to the ``.git/`` directory pointing to the ref in question,
62
+ e.g. ``refs/heads/master``.
63
+
64
+ :param check_path:
65
+ If ``False``, you can provide any path.
66
+ Otherwise the path must start with the default path prefix of this type.
67
+ """
68
+ if check_path and not str(path).startswith(self._common_path_default + "/"):
69
+ raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
70
+ self.path: str # SymbolicReference converts to string at the moment.
71
+ super().__init__(repo, path)
72
+
73
+ def __str__(self) -> str:
74
+ return self.name
75
+
76
+ # { Interface
77
+
78
+ # @ReservedAssignment
79
+ def set_object(
80
+ self,
81
+ object: Union[AnyGitObject, "SymbolicReference", str],
82
+ logmsg: Union[str, None] = None,
83
+ ) -> "Reference":
84
+ """Special version which checks if the head-log needs an update as well.
85
+
86
+ :return:
87
+ self
88
+ """
89
+ oldbinsha = None
90
+ if logmsg is not None:
91
+ head = self.repo.head
92
+ if not head.is_detached and head.ref == self:
93
+ oldbinsha = self.commit.binsha
94
+ # END handle commit retrieval
95
+ # END handle message is set
96
+
97
+ super().set_object(object, logmsg)
98
+
99
+ if oldbinsha is not None:
100
+ # From refs/files-backend.c in git-source:
101
+ # /*
102
+ # * Special hack: If a branch is updated directly and HEAD
103
+ # * points to it (may happen on the remote side of a push
104
+ # * for example) then logically the HEAD reflog should be
105
+ # * updated too.
106
+ # * A generic solution implies reverse symref information,
107
+ # * but finding all symrefs pointing to the given branch
108
+ # * would be rather costly for this rare event (the direct
109
+ # * update of a branch) to be worth it. So let's cheat and
110
+ # * check with HEAD only which should cover 99% of all usage
111
+ # * scenarios (even 100% of the default ones).
112
+ # */
113
+ self.repo.head.log_append(oldbinsha, logmsg)
114
+ # END check if the head
115
+
116
+ return self
117
+
118
+ # NOTE: No need to overwrite properties, as the will only work without a the log.
119
+
120
+ @property
121
+ def name(self) -> str:
122
+ """
123
+ :return:
124
+ (shortest) Name of this reference - it may contain path components
125
+ """
126
+ # The first two path tokens can be removed as they are
127
+ # refs/heads or refs/tags or refs/remotes.
128
+ tokens = self.path.split("/")
129
+ if len(tokens) < 3:
130
+ return self.path # could be refs/HEAD
131
+ return "/".join(tokens[2:])
132
+
133
+ @classmethod
134
+ def iter_items(
135
+ cls: Type[T_References],
136
+ repo: "Repo",
137
+ common_path: Union[PathLike, None] = None,
138
+ *args: Any,
139
+ **kwargs: Any,
140
+ ) -> Iterator[T_References]:
141
+ """Equivalent to
142
+ :meth:`SymbolicReference.iter_items <git.refs.symbolic.SymbolicReference.iter_items>`,
143
+ but will return non-detached references as well."""
144
+ return cls._iter_items(repo, common_path)
145
+
146
+ # } END interface
147
+
148
+ # { Remote Interface
149
+
150
+ @property
151
+ @require_remote_ref_path
152
+ def remote_name(self) -> str:
153
+ """
154
+ :return:
155
+ Name of the remote we are a reference of, such as ``origin`` for a reference
156
+ named ``origin/master``.
157
+ """
158
+ tokens = self.path.split("/")
159
+ # /refs/remotes/<remote name>/<branch_name>
160
+ return tokens[2]
161
+
162
+ @property
163
+ @require_remote_ref_path
164
+ def remote_head(self) -> str:
165
+ """
166
+ :return:
167
+ Name of the remote head itself, e.g. ``master``.
168
+
169
+ :note:
170
+ The returned name is usually not qualified enough to uniquely identify a
171
+ branch.
172
+ """
173
+ tokens = self.path.split("/")
174
+ return "/".join(tokens[3:])
175
+
176
+ # } END remote interface
.venv/lib/python3.11/site-packages/git/refs/remote.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Module implementing a remote object allowing easy access to git remotes."""
5
+
6
+ __all__ = ["RemoteReference"]
7
+
8
+ import os
9
+
10
+ from git.util import join_path
11
+
12
+ from .head import Head
13
+
14
+ # typing ------------------------------------------------------------------
15
+
16
+ from typing import Any, Iterator, NoReturn, TYPE_CHECKING, Union
17
+
18
+ from git.types import PathLike
19
+
20
+ if TYPE_CHECKING:
21
+ from git.remote import Remote
22
+ from git.repo import Repo
23
+
24
+ # ------------------------------------------------------------------------------
25
+
26
+
27
+ class RemoteReference(Head):
28
+ """A reference pointing to a remote head."""
29
+
30
+ _common_path_default = Head._remote_common_path_default
31
+
32
+ @classmethod
33
+ def iter_items(
34
+ cls,
35
+ repo: "Repo",
36
+ common_path: Union[PathLike, None] = None,
37
+ remote: Union["Remote", None] = None,
38
+ *args: Any,
39
+ **kwargs: Any,
40
+ ) -> Iterator["RemoteReference"]:
41
+ """Iterate remote references, and if given, constrain them to the given remote."""
42
+ common_path = common_path or cls._common_path_default
43
+ if remote is not None:
44
+ common_path = join_path(common_path, str(remote))
45
+ # END handle remote constraint
46
+ # super is Reference
47
+ return super().iter_items(repo, common_path)
48
+
49
+ # The Head implementation of delete also accepts strs, but this implementation does
50
+ # not. mypy doesn't have a way of representing tightening the types of arguments in
51
+ # subclasses and recommends Any or "type: ignore".
52
+ # (See: https://github.com/python/typing/issues/241)
53
+ @classmethod
54
+ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: # type: ignore[override]
55
+ """Delete the given remote references.
56
+
57
+ :note:
58
+ `kwargs` are given for comparability with the base class method as we
59
+ should not narrow the signature.
60
+ """
61
+ repo.git.branch("-d", "-r", *refs)
62
+ # The official deletion method will ignore remote symbolic refs - these are
63
+ # generally ignored in the refs/ folder. We don't though and delete remainders
64
+ # manually.
65
+ for ref in refs:
66
+ try:
67
+ os.remove(os.path.join(repo.common_dir, ref.path))
68
+ except OSError:
69
+ pass
70
+ try:
71
+ os.remove(os.path.join(repo.git_dir, ref.path))
72
+ except OSError:
73
+ pass
74
+ # END for each ref
75
+
76
+ @classmethod
77
+ def create(cls, *args: Any, **kwargs: Any) -> NoReturn:
78
+ """Raise :exc:`TypeError`. Defined so the ``create`` method is disabled."""
79
+ raise TypeError("Cannot explicitly create remote references")
.venv/lib/python3.11/site-packages/git/refs/symbolic.py ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ __all__ = ["SymbolicReference"]
5
+
6
+ import os
7
+
8
+ from gitdb.exc import BadName, BadObject
9
+
10
+ from git.compat import defenc
11
+ from git.objects.base import Object
12
+ from git.objects.commit import Commit
13
+ from git.refs.log import RefLog
14
+ from git.util import (
15
+ LockedFD,
16
+ assure_directory_exists,
17
+ hex_to_bin,
18
+ join_path,
19
+ join_path_native,
20
+ to_native_path_linux,
21
+ )
22
+
23
+ # typing ------------------------------------------------------------------
24
+
25
+ from typing import (
26
+ Any,
27
+ Iterator,
28
+ List,
29
+ TYPE_CHECKING,
30
+ Tuple,
31
+ Type,
32
+ TypeVar,
33
+ Union,
34
+ cast,
35
+ )
36
+
37
+ from git.types import AnyGitObject, PathLike
38
+
39
+ if TYPE_CHECKING:
40
+ from git.config import GitConfigParser
41
+ from git.objects.commit import Actor
42
+ from git.refs.log import RefLogEntry
43
+ from git.repo import Repo
44
+
45
+
46
+ T_References = TypeVar("T_References", bound="SymbolicReference")
47
+
48
+ # ------------------------------------------------------------------------------
49
+
50
+
51
+ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
52
+ """Find the git dir that is appropriate for the path."""
53
+ name = f"{path}"
54
+ if name in ["HEAD", "ORIG_HEAD", "FETCH_HEAD", "index", "logs"]:
55
+ return repo.git_dir
56
+ return repo.common_dir
57
+
58
+
59
+ class SymbolicReference:
60
+ """Special case of a reference that is symbolic.
61
+
62
+ This does not point to a specific commit, but to another
63
+ :class:`~git.refs.head.Head`, which itself specifies a commit.
64
+
65
+ A typical example for a symbolic reference is :class:`~git.refs.head.HEAD`.
66
+ """
67
+
68
+ __slots__ = ("repo", "path")
69
+
70
+ _resolve_ref_on_create = False
71
+ _points_to_commits_only = True
72
+ _common_path_default = ""
73
+ _remote_common_path_default = "refs/remotes"
74
+ _id_attribute_ = "name"
75
+
76
+ def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False) -> None:
77
+ self.repo = repo
78
+ self.path = path
79
+
80
+ def __str__(self) -> str:
81
+ return str(self.path)
82
+
83
+ def __repr__(self) -> str:
84
+ return '<git.%s "%s">' % (self.__class__.__name__, self.path)
85
+
86
+ def __eq__(self, other: object) -> bool:
87
+ if hasattr(other, "path"):
88
+ other = cast(SymbolicReference, other)
89
+ return self.path == other.path
90
+ return False
91
+
92
+ def __ne__(self, other: object) -> bool:
93
+ return not (self == other)
94
+
95
+ def __hash__(self) -> int:
96
+ return hash(self.path)
97
+
98
+ @property
99
+ def name(self) -> str:
100
+ """
101
+ :return:
102
+ In case of symbolic references, the shortest assumable name is the path
103
+ itself.
104
+ """
105
+ return str(self.path)
106
+
107
+ @property
108
+ def abspath(self) -> PathLike:
109
+ return join_path_native(_git_dir(self.repo, self.path), self.path)
110
+
111
+ @classmethod
112
+ def _get_packed_refs_path(cls, repo: "Repo") -> str:
113
+ return os.path.join(repo.common_dir, "packed-refs")
114
+
115
+ @classmethod
116
+ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
117
+ """Return an iterator yielding pairs of sha1/path pairs (as strings) for the
118
+ corresponding refs.
119
+
120
+ :note:
121
+ The packed refs file will be kept open as long as we iterate.
122
+ """
123
+ try:
124
+ with open(cls._get_packed_refs_path(repo), "rt", encoding="UTF-8") as fp:
125
+ for line in fp:
126
+ line = line.strip()
127
+ if not line:
128
+ continue
129
+ if line.startswith("#"):
130
+ # "# pack-refs with: peeled fully-peeled sorted"
131
+ # the git source code shows "peeled",
132
+ # "fully-peeled" and "sorted" as the keywords
133
+ # that can go on this line, as per comments in git file
134
+ # refs/packed-backend.c
135
+ # I looked at master on 2017-10-11,
136
+ # commit 111ef79afe, after tag v2.15.0-rc1
137
+ # from repo https://github.com/git/git.git
138
+ if line.startswith("# pack-refs with:") and "peeled" not in line:
139
+ raise TypeError("PackingType of packed-Refs not understood: %r" % line)
140
+ # END abort if we do not understand the packing scheme
141
+ continue
142
+ # END parse comment
143
+
144
+ # Skip dereferenced tag object entries - previous line was actual
145
+ # tag reference for it.
146
+ if line[0] == "^":
147
+ continue
148
+
149
+ yield cast(Tuple[str, str], tuple(line.split(" ", 1)))
150
+ # END for each line
151
+ except OSError:
152
+ return None
153
+ # END no packed-refs file handling
154
+
155
+ @classmethod
156
+ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> str:
157
+ """
158
+ :return:
159
+ hexsha stored in the reference at the given `ref_path`, recursively
160
+ dereferencing all intermediate references as required
161
+
162
+ :param repo:
163
+ The repository containing the reference at `ref_path`.
164
+ """
165
+
166
+ while True:
167
+ hexsha, ref_path = cls._get_ref_info(repo, ref_path)
168
+ if hexsha is not None:
169
+ return hexsha
170
+ # END recursive dereferencing
171
+
172
+ @staticmethod
173
+ def _check_ref_name_valid(ref_path: PathLike) -> None:
174
+ """Check a ref name for validity.
175
+
176
+ This is based on the rules described in :manpage:`git-check-ref-format(1)`.
177
+ """
178
+ previous: Union[str, None] = None
179
+ one_before_previous: Union[str, None] = None
180
+ for c in str(ref_path):
181
+ if c in " ~^:?*[\\":
182
+ raise ValueError(
183
+ f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^),"
184
+ f" colons (:), question marks (?), asterisks (*), open brackets ([) or backslashes (\\)"
185
+ )
186
+ elif c == ".":
187
+ if previous is None or previous == "/":
188
+ raise ValueError(
189
+ f"Invalid reference '{ref_path}': references cannot start with a period (.) or contain '/.'"
190
+ )
191
+ elif previous == ".":
192
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '..'")
193
+ elif c == "/":
194
+ if previous == "/":
195
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '//'")
196
+ elif previous is None:
197
+ raise ValueError(
198
+ f"Invalid reference '{ref_path}': references cannot start with forward slashes '/'"
199
+ )
200
+ elif c == "{" and previous == "@":
201
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '@{{'")
202
+ elif ord(c) < 32 or ord(c) == 127:
203
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot contain ASCII control characters")
204
+
205
+ one_before_previous = previous
206
+ previous = c
207
+
208
+ if previous == ".":
209
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a period (.)")
210
+ elif previous == "/":
211
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)")
212
+ elif previous == "@" and one_before_previous is None:
213
+ raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'")
214
+ elif any(component.endswith(".lock") for component in str(ref_path).split("/")):
215
+ raise ValueError(
216
+ f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with"
217
+ " '.lock'"
218
+ )
219
+
220
+ @classmethod
221
+ def _get_ref_info_helper(
222
+ cls, repo: "Repo", ref_path: Union[PathLike, None]
223
+ ) -> Union[Tuple[str, None], Tuple[None, str]]:
224
+ """
225
+ :return:
226
+ *(str(sha), str(target_ref_path))*, where:
227
+
228
+ * *sha* is of the file at rela_path points to if available, or ``None``.
229
+ * *target_ref_path* is the reference we point to, or ``None``.
230
+ """
231
+ if ref_path:
232
+ cls._check_ref_name_valid(ref_path)
233
+
234
+ tokens: Union[None, List[str], Tuple[str, str]] = None
235
+ repodir = _git_dir(repo, ref_path)
236
+ try:
237
+ with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp:
238
+ value = fp.read().rstrip()
239
+ # Don't only split on spaces, but on whitespace, which allows to parse lines like:
240
+ # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo
241
+ tokens = value.split()
242
+ assert len(tokens) != 0
243
+ except OSError:
244
+ # Probably we are just packed. Find our entry in the packed refs file.
245
+ # NOTE: We are not a symbolic ref if we are in a packed file, as these
246
+ # are excluded explicitly.
247
+ for sha, path in cls._iter_packed_refs(repo):
248
+ if path != ref_path:
249
+ continue
250
+ # sha will be used.
251
+ tokens = sha, path
252
+ break
253
+ # END for each packed ref
254
+ # END handle packed refs
255
+ if tokens is None:
256
+ raise ValueError("Reference at %r does not exist" % ref_path)
257
+
258
+ # Is it a reference?
259
+ if tokens[0] == "ref:":
260
+ return (None, tokens[1])
261
+
262
+ # It's a commit.
263
+ if repo.re_hexsha_only.match(tokens[0]):
264
+ return (tokens[0], None)
265
+
266
+ raise ValueError("Failed to parse reference information from %r" % ref_path)
267
+
268
+ @classmethod
269
+ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[Tuple[str, None], Tuple[None, str]]:
270
+ """
271
+ :return:
272
+ *(str(sha), str(target_ref_path))*, where:
273
+
274
+ * *sha* is of the file at rela_path points to if available, or ``None``.
275
+ * *target_ref_path* is the reference we point to, or ``None``.
276
+ """
277
+ return cls._get_ref_info_helper(repo, ref_path)
278
+
279
+ def _get_object(self) -> AnyGitObject:
280
+ """
281
+ :return:
282
+ The object our ref currently refers to. Refs can be cached, they will always
283
+ point to the actual object as it gets re-created on each query.
284
+ """
285
+ # We have to be dynamic here as we may be a tag which can point to anything.
286
+ # Our path will be resolved to the hexsha which will be used accordingly.
287
+ return Object.new_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path)))
288
+
289
+ def _get_commit(self) -> "Commit":
290
+ """
291
+ :return:
292
+ :class:`~git.objects.commit.Commit` object we point to. This works for
293
+ detached and non-detached :class:`SymbolicReference` instances. The symbolic
294
+ reference will be dereferenced recursively.
295
+ """
296
+ obj = self._get_object()
297
+ if obj.type == "tag":
298
+ obj = obj.object
299
+ # END dereference tag
300
+
301
+ if obj.type != Commit.type:
302
+ raise TypeError("Symbolic Reference pointed to object %r, commit was required" % obj)
303
+ # END handle type
304
+ return obj
305
+
306
+ def set_commit(
307
+ self,
308
+ commit: Union[Commit, "SymbolicReference", str],
309
+ logmsg: Union[str, None] = None,
310
+ ) -> "SymbolicReference":
311
+ """Like :meth:`set_object`, but restricts the type of object to be a
312
+ :class:`~git.objects.commit.Commit`.
313
+
314
+ :raise ValueError:
315
+ If `commit` is not a :class:`~git.objects.commit.Commit` object, nor does it
316
+ point to a commit.
317
+
318
+ :return:
319
+ self
320
+ """
321
+ # Check the type - assume the best if it is a base-string.
322
+ invalid_type = False
323
+ if isinstance(commit, Object):
324
+ invalid_type = commit.type != Commit.type
325
+ elif isinstance(commit, SymbolicReference):
326
+ invalid_type = commit.object.type != Commit.type
327
+ else:
328
+ try:
329
+ invalid_type = self.repo.rev_parse(commit).type != Commit.type
330
+ except (BadObject, BadName) as e:
331
+ raise ValueError("Invalid object: %s" % commit) from e
332
+ # END handle exception
333
+ # END verify type
334
+
335
+ if invalid_type:
336
+ raise ValueError("Need commit, got %r" % commit)
337
+ # END handle raise
338
+
339
+ # We leave strings to the rev-parse method below.
340
+ self.set_object(commit, logmsg)
341
+
342
+ return self
343
+
344
+ def set_object(
345
+ self,
346
+ object: Union[AnyGitObject, "SymbolicReference", str],
347
+ logmsg: Union[str, None] = None,
348
+ ) -> "SymbolicReference":
349
+ """Set the object we point to, possibly dereference our symbolic reference
350
+ first. If the reference does not exist, it will be created.
351
+
352
+ :param object:
353
+ A refspec, a :class:`SymbolicReference` or an
354
+ :class:`~git.objects.base.Object` instance.
355
+
356
+ * :class:`SymbolicReference` instances will be dereferenced beforehand to
357
+ obtain the git object they point to.
358
+ * :class:`~git.objects.base.Object` instances must represent git objects
359
+ (:class:`~git.types.AnyGitObject`).
360
+
361
+ :param logmsg:
362
+ If not ``None``, the message will be used in the reflog entry to be written.
363
+ Otherwise the reflog is not altered.
364
+
365
+ :note:
366
+ Plain :class:`SymbolicReference` instances may not actually point to objects
367
+ by convention.
368
+
369
+ :return:
370
+ self
371
+ """
372
+ if isinstance(object, SymbolicReference):
373
+ object = object.object # @ReservedAssignment
374
+ # END resolve references
375
+
376
+ is_detached = True
377
+ try:
378
+ is_detached = self.is_detached
379
+ except ValueError:
380
+ pass
381
+ # END handle non-existing ones
382
+
383
+ if is_detached:
384
+ return self.set_reference(object, logmsg)
385
+
386
+ # set the commit on our reference
387
+ return self._get_reference().set_object(object, logmsg)
388
+
389
+ @property
390
+ def commit(self) -> "Commit":
391
+ """Query or set commits directly"""
392
+ return self._get_commit()
393
+
394
+ @commit.setter
395
+ def commit(self, commit: Union[Commit, "SymbolicReference", str]) -> "SymbolicReference":
396
+ return self.set_commit(commit)
397
+
398
+ @property
399
+ def object(self) -> AnyGitObject:
400
+ """Return the object our ref currently refers to"""
401
+ return self._get_object()
402
+
403
+ @object.setter
404
+ def object(self, object: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference":
405
+ return self.set_object(object)
406
+
407
+ def _get_reference(self) -> "SymbolicReference":
408
+ """
409
+ :return:
410
+ :class:`~git.refs.reference.Reference` object we point to
411
+
412
+ :raise TypeError:
413
+ If this symbolic reference is detached, hence it doesn't point to a
414
+ reference, but to a commit.
415
+ """
416
+ sha, target_ref_path = self._get_ref_info(self.repo, self.path)
417
+ if target_ref_path is None:
418
+ raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha))
419
+ return self.from_path(self.repo, target_ref_path)
420
+
421
+ def set_reference(
422
+ self,
423
+ ref: Union[AnyGitObject, "SymbolicReference", str],
424
+ logmsg: Union[str, None] = None,
425
+ ) -> "SymbolicReference":
426
+ """Set ourselves to the given `ref`.
427
+
428
+ It will stay a symbol if the `ref` is a :class:`~git.refs.reference.Reference`.
429
+
430
+ Otherwise a git object, specified as a :class:`~git.objects.base.Object`
431
+ instance or refspec, is assumed. If it is valid, this reference will be set to
432
+ it, which effectively detaches the reference if it was a purely symbolic one.
433
+
434
+ :param ref:
435
+ A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object`
436
+ instance (specifically an :class:`~git.types.AnyGitObject`), or a refspec
437
+ string. Only if the ref is a :class:`SymbolicReference` instance, we will
438
+ point to it. Everything else is dereferenced to obtain the actual object.
439
+
440
+ :param logmsg:
441
+ If set to a string, the message will be used in the reflog.
442
+ Otherwise, a reflog entry is not written for the changed reference.
443
+ The previous commit of the entry will be the commit we point to now.
444
+
445
+ See also: :meth:`log_append`
446
+
447
+ :return:
448
+ self
449
+
450
+ :note:
451
+ This symbolic reference will not be dereferenced. For that, see
452
+ :meth:`set_object`.
453
+ """
454
+ write_value = None
455
+ obj = None
456
+ if isinstance(ref, SymbolicReference):
457
+ write_value = "ref: %s" % ref.path
458
+ elif isinstance(ref, Object):
459
+ obj = ref
460
+ write_value = ref.hexsha
461
+ elif isinstance(ref, str):
462
+ try:
463
+ obj = self.repo.rev_parse(ref + "^{}") # Optionally dereference tags.
464
+ write_value = obj.hexsha
465
+ except (BadObject, BadName) as e:
466
+ raise ValueError("Could not extract object from %s" % ref) from e
467
+ # END end try string
468
+ else:
469
+ raise ValueError("Unrecognized Value: %r" % ref)
470
+ # END try commit attribute
471
+
472
+ # typecheck
473
+ if obj is not None and self._points_to_commits_only and obj.type != Commit.type:
474
+ raise TypeError("Require commit, got %r" % obj)
475
+ # END verify type
476
+
477
+ oldbinsha: bytes = b""
478
+ if logmsg is not None:
479
+ try:
480
+ oldbinsha = self.commit.binsha
481
+ except ValueError:
482
+ oldbinsha = Commit.NULL_BIN_SHA
483
+ # END handle non-existing
484
+ # END retrieve old hexsha
485
+
486
+ fpath = self.abspath
487
+ assure_directory_exists(fpath, is_file=True)
488
+
489
+ lfd = LockedFD(fpath)
490
+ fd = lfd.open(write=True, stream=True)
491
+ try:
492
+ fd.write(write_value.encode("utf-8") + b"\n")
493
+ lfd.commit()
494
+ except BaseException:
495
+ lfd.rollback()
496
+ raise
497
+ # Adjust the reflog
498
+ if logmsg is not None:
499
+ self.log_append(oldbinsha, logmsg)
500
+
501
+ return self
502
+
503
+ # Aliased reference
504
+ @property
505
+ def reference(self) -> "SymbolicReference":
506
+ return self._get_reference()
507
+
508
+ @reference.setter
509
+ def reference(self, ref: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference":
510
+ return self.set_reference(ref)
511
+
512
+ ref = reference
513
+
514
+ def is_valid(self) -> bool:
515
+ """
516
+ :return:
517
+ ``True`` if the reference is valid, hence it can be read and points to a
518
+ valid object or reference.
519
+ """
520
+ try:
521
+ self.object # noqa: B018
522
+ except (OSError, ValueError):
523
+ return False
524
+ else:
525
+ return True
526
+
527
+ @property
528
+ def is_detached(self) -> bool:
529
+ """
530
+ :return:
531
+ ``True`` if we are a detached reference, hence we point to a specific commit
532
+ instead to another reference.
533
+ """
534
+ try:
535
+ self.ref # noqa: B018
536
+ return False
537
+ except TypeError:
538
+ return True
539
+
540
+ def log(self) -> "RefLog":
541
+ """
542
+ :return:
543
+ :class:`~git.refs.log.RefLog` for this reference.
544
+ Its last entry reflects the latest change applied to this reference.
545
+
546
+ :note:
547
+ As the log is parsed every time, its recommended to cache it for use instead
548
+ of calling this method repeatedly. It should be considered read-only.
549
+ """
550
+ return RefLog.from_file(RefLog.path(self))
551
+
552
+ def log_append(
553
+ self,
554
+ oldbinsha: bytes,
555
+ message: Union[str, None],
556
+ newbinsha: Union[bytes, None] = None,
557
+ ) -> "RefLogEntry":
558
+ """Append a logentry to the logfile of this ref.
559
+
560
+ :param oldbinsha:
561
+ Binary sha this ref used to point to.
562
+
563
+ :param message:
564
+ A message describing the change.
565
+
566
+ :param newbinsha:
567
+ The sha the ref points to now. If None, our current commit sha will be used.
568
+
569
+ :return:
570
+ The added :class:`~git.refs.log.RefLogEntry` instance.
571
+ """
572
+ # NOTE: We use the committer of the currently active commit - this should be
573
+ # correct to allow overriding the committer on a per-commit level.
574
+ # See https://github.com/gitpython-developers/GitPython/pull/146.
575
+ try:
576
+ committer_or_reader: Union["Actor", "GitConfigParser"] = self.commit.committer
577
+ except ValueError:
578
+ committer_or_reader = self.repo.config_reader()
579
+ # END handle newly cloned repositories
580
+ if newbinsha is None:
581
+ newbinsha = self.commit.binsha
582
+
583
+ if message is None:
584
+ message = ""
585
+
586
+ return RefLog.append_entry(committer_or_reader, RefLog.path(self), oldbinsha, newbinsha, message)
587
+
588
+ def log_entry(self, index: int) -> "RefLogEntry":
589
+ """
590
+ :return:
591
+ :class:`~git.refs.log.RefLogEntry` at the given index
592
+
593
+ :param index:
594
+ Python list compatible positive or negative index.
595
+
596
+ :note:
597
+ This method must read part of the reflog during execution, hence it should
598
+ be used sparingly, or only if you need just one index. In that case, it will
599
+ be faster than the :meth:`log` method.
600
+ """
601
+ return RefLog.entry_at(RefLog.path(self), index)
602
+
603
+ @classmethod
604
+ def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
605
+ """
606
+ :return:
607
+ String with a full repository-relative path which can be used to initialize
608
+ a :class:`~git.refs.reference.Reference` instance, for instance by using
609
+ :meth:`Reference.from_path <git.refs.reference.Reference.from_path>`.
610
+ """
611
+ if isinstance(path, SymbolicReference):
612
+ path = path.path
613
+ full_ref_path = path
614
+ if not cls._common_path_default:
615
+ return full_ref_path
616
+ if not str(path).startswith(cls._common_path_default + "/"):
617
+ full_ref_path = "%s/%s" % (cls._common_path_default, path)
618
+ return full_ref_path
619
+
620
+ @classmethod
621
+ def delete(cls, repo: "Repo", path: PathLike) -> None:
622
+ """Delete the reference at the given path.
623
+
624
+ :param repo:
625
+ Repository to delete the reference from.
626
+
627
+ :param path:
628
+ Short or full path pointing to the reference, e.g. ``refs/myreference`` or
629
+ just ``myreference``, hence ``refs/`` is implied.
630
+ Alternatively the symbolic reference to be deleted.
631
+ """
632
+ full_ref_path = cls.to_full_path(path)
633
+ abs_path = os.path.join(repo.common_dir, full_ref_path)
634
+ if os.path.exists(abs_path):
635
+ os.remove(abs_path)
636
+ else:
637
+ # Check packed refs.
638
+ pack_file_path = cls._get_packed_refs_path(repo)
639
+ try:
640
+ with open(pack_file_path, "rb") as reader:
641
+ new_lines = []
642
+ made_change = False
643
+ dropped_last_line = False
644
+ for line_bytes in reader:
645
+ line = line_bytes.decode(defenc)
646
+ _, _, line_ref = line.partition(" ")
647
+ line_ref = line_ref.strip()
648
+ # Keep line if it is a comment or if the ref to delete is not in
649
+ # the line.
650
+ # If we deleted the last line and this one is a tag-reference
651
+ # object, we drop it as well.
652
+ if (line.startswith("#") or full_ref_path != line_ref) and (
653
+ not dropped_last_line or dropped_last_line and not line.startswith("^")
654
+ ):
655
+ new_lines.append(line)
656
+ dropped_last_line = False
657
+ continue
658
+ # END skip comments and lines without our path
659
+
660
+ # Drop this line.
661
+ made_change = True
662
+ dropped_last_line = True
663
+
664
+ # Write the new lines.
665
+ if made_change:
666
+ # Binary writing is required, otherwise Windows will open the file
667
+ # in text mode and change LF to CRLF!
668
+ with open(pack_file_path, "wb") as fd:
669
+ fd.writelines(line.encode(defenc) for line in new_lines)
670
+
671
+ except OSError:
672
+ pass # It didn't exist at all.
673
+
674
+ # Delete the reflog.
675
+ reflog_path = RefLog.path(cls(repo, full_ref_path))
676
+ if os.path.isfile(reflog_path):
677
+ os.remove(reflog_path)
678
+ # END remove reflog
679
+
680
+ @classmethod
681
+ def _create(
682
+ cls: Type[T_References],
683
+ repo: "Repo",
684
+ path: PathLike,
685
+ resolve: bool,
686
+ reference: Union["SymbolicReference", str],
687
+ force: bool,
688
+ logmsg: Union[str, None] = None,
689
+ ) -> T_References:
690
+ """Internal method used to create a new symbolic reference.
691
+
692
+ If `resolve` is ``False``, the reference will be taken as is, creating a proper
693
+ symbolic reference. Otherwise it will be resolved to the corresponding object
694
+ and a detached symbolic reference will be created instead.
695
+ """
696
+ git_dir = _git_dir(repo, path)
697
+ full_ref_path = cls.to_full_path(path)
698
+ abs_ref_path = os.path.join(git_dir, full_ref_path)
699
+
700
+ # Figure out target data.
701
+ target = reference
702
+ if resolve:
703
+ target = repo.rev_parse(str(reference))
704
+
705
+ if not force and os.path.isfile(abs_ref_path):
706
+ target_data = str(target)
707
+ if isinstance(target, SymbolicReference):
708
+ target_data = str(target.path)
709
+ if not resolve:
710
+ target_data = "ref: " + target_data
711
+ with open(abs_ref_path, "rb") as fd:
712
+ existing_data = fd.read().decode(defenc).strip()
713
+ if existing_data != target_data:
714
+ raise OSError(
715
+ "Reference at %r does already exist, pointing to %r, requested was %r"
716
+ % (full_ref_path, existing_data, target_data)
717
+ )
718
+ # END no force handling
719
+
720
+ ref = cls(repo, full_ref_path)
721
+ ref.set_reference(target, logmsg)
722
+ return ref
723
+
724
+ @classmethod
725
+ def create(
726
+ cls: Type[T_References],
727
+ repo: "Repo",
728
+ path: PathLike,
729
+ reference: Union["SymbolicReference", str] = "HEAD",
730
+ logmsg: Union[str, None] = None,
731
+ force: bool = False,
732
+ **kwargs: Any,
733
+ ) -> T_References:
734
+ """Create a new symbolic reference: a reference pointing to another reference.
735
+
736
+ :param repo:
737
+ Repository to create the reference in.
738
+
739
+ :param path:
740
+ Full path at which the new symbolic reference is supposed to be created at,
741
+ e.g. ``NEW_HEAD`` or ``symrefs/my_new_symref``.
742
+
743
+ :param reference:
744
+ The reference which the new symbolic reference should point to.
745
+ If it is a commit-ish, the symbolic ref will be detached.
746
+
747
+ :param force:
748
+ If ``True``, force creation even if a symbolic reference with that name
749
+ already exists. Raise :exc:`OSError` otherwise.
750
+
751
+ :param logmsg:
752
+ If not ``None``, the message to append to the reflog.
753
+ If ``None``, no reflog entry is written.
754
+
755
+ :return:
756
+ Newly created symbolic reference
757
+
758
+ :raise OSError:
759
+ If a (Symbolic)Reference with the same name but different contents already
760
+ exists.
761
+
762
+ :note:
763
+ This does not alter the current HEAD, index or working tree.
764
+ """
765
+ return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg)
766
+
767
+ def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference":
768
+ """Rename self to a new path.
769
+
770
+ :param new_path:
771
+ Either a simple name or a full path, e.g. ``new_name`` or
772
+ ``features/new_name``.
773
+ The prefix ``refs/`` is implied for references and will be set as needed.
774
+ In case this is a symbolic ref, there is no implied prefix.
775
+
776
+ :param force:
777
+ If ``True``, the rename will succeed even if a head with the target name
778
+ already exists. It will be overwritten in that case.
779
+
780
+ :return:
781
+ self
782
+
783
+ :raise OSError:
784
+ If a file at path but with different contents already exists.
785
+ """
786
+ new_path = self.to_full_path(new_path)
787
+ if self.path == new_path:
788
+ return self
789
+
790
+ new_abs_path = os.path.join(_git_dir(self.repo, new_path), new_path)
791
+ cur_abs_path = os.path.join(_git_dir(self.repo, self.path), self.path)
792
+ if os.path.isfile(new_abs_path):
793
+ if not force:
794
+ # If they point to the same file, it's not an error.
795
+ with open(new_abs_path, "rb") as fd1:
796
+ f1 = fd1.read().strip()
797
+ with open(cur_abs_path, "rb") as fd2:
798
+ f2 = fd2.read().strip()
799
+ if f1 != f2:
800
+ raise OSError("File at path %r already exists" % new_abs_path)
801
+ # else: We could remove ourselves and use the other one, but...
802
+ # ...for clarity, we just continue as usual.
803
+ # END not force handling
804
+ os.remove(new_abs_path)
805
+ # END handle existing target file
806
+
807
+ dname = os.path.dirname(new_abs_path)
808
+ if not os.path.isdir(dname):
809
+ os.makedirs(dname)
810
+ # END create directory
811
+
812
+ os.rename(cur_abs_path, new_abs_path)
813
+ self.path = new_path
814
+
815
+ return self
816
+
817
+ @classmethod
818
+ def _iter_items(
819
+ cls: Type[T_References], repo: "Repo", common_path: Union[PathLike, None] = None
820
+ ) -> Iterator[T_References]:
821
+ if common_path is None:
822
+ common_path = cls._common_path_default
823
+ rela_paths = set()
824
+
825
+ # Walk loose refs.
826
+ # Currently we do not follow links.
827
+ for root, dirs, files in os.walk(join_path_native(repo.common_dir, common_path)):
828
+ if "refs" not in root.split(os.sep): # Skip non-refs subfolders.
829
+ refs_id = [d for d in dirs if d == "refs"]
830
+ if refs_id:
831
+ dirs[0:] = ["refs"]
832
+ # END prune non-refs folders
833
+
834
+ for f in files:
835
+ if f == "packed-refs":
836
+ continue
837
+ abs_path = to_native_path_linux(join_path(root, f))
838
+ rela_paths.add(abs_path.replace(to_native_path_linux(repo.common_dir) + "/", ""))
839
+ # END for each file in root directory
840
+ # END for each directory to walk
841
+
842
+ # Read packed refs.
843
+ for _sha, rela_path in cls._iter_packed_refs(repo):
844
+ if rela_path.startswith(str(common_path)):
845
+ rela_paths.add(rela_path)
846
+ # END relative path matches common path
847
+ # END packed refs reading
848
+
849
+ # Yield paths in sorted order.
850
+ for path in sorted(rela_paths):
851
+ try:
852
+ yield cls.from_path(repo, path)
853
+ except ValueError:
854
+ continue
855
+ # END for each sorted relative refpath
856
+
857
+ @classmethod
858
+ def iter_items(
859
+ cls: Type[T_References],
860
+ repo: "Repo",
861
+ common_path: Union[PathLike, None] = None,
862
+ *args: Any,
863
+ **kwargs: Any,
864
+ ) -> Iterator[T_References]:
865
+ """Find all refs in the repository.
866
+
867
+ :param repo:
868
+ The :class:`~git.repo.base.Repo`.
869
+
870
+ :param common_path:
871
+ Optional keyword argument to the path which is to be shared by all returned
872
+ Ref objects.
873
+ Defaults to class specific portion if ``None``, ensuring that only refs
874
+ suitable for the actual class are returned.
875
+
876
+ :return:
877
+ A list of :class:`SymbolicReference`, each guaranteed to be a symbolic ref
878
+ which is not detached and pointing to a valid ref.
879
+
880
+ The list is lexicographically sorted. The returned objects are instances of
881
+ concrete subclasses, such as :class:`~git.refs.head.Head` or
882
+ :class:`~git.refs.tag.TagReference`.
883
+ """
884
+ return (r for r in cls._iter_items(repo, common_path) if r.__class__ is SymbolicReference or not r.is_detached)
885
+
886
+ @classmethod
887
+ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_References:
888
+ """Make a symbolic reference from a path.
889
+
890
+ :param path:
891
+ Full ``.git``-directory-relative path name to the Reference to instantiate.
892
+
893
+ :note:
894
+ Use :meth:`to_full_path` if you only have a partial path of a known
895
+ Reference type.
896
+
897
+ :return:
898
+ Instance of type :class:`~git.refs.reference.Reference`,
899
+ :class:`~git.refs.head.Head`, or :class:`~git.refs.tag.Tag`, depending on
900
+ the given path.
901
+ """
902
+ if not path:
903
+ raise ValueError("Cannot create Reference from %r" % path)
904
+
905
+ # Names like HEAD are inserted after the refs module is imported - we have an
906
+ # import dependency cycle and don't want to import these names in-function.
907
+ from . import HEAD, Head, RemoteReference, TagReference, Reference
908
+
909
+ for ref_type in (
910
+ HEAD,
911
+ Head,
912
+ RemoteReference,
913
+ TagReference,
914
+ Reference,
915
+ SymbolicReference,
916
+ ):
917
+ try:
918
+ instance: T_References
919
+ instance = ref_type(repo, path)
920
+ if instance.__class__ is SymbolicReference and instance.is_detached:
921
+ raise ValueError("SymbolicRef was detached, we drop it")
922
+ else:
923
+ return instance
924
+
925
+ except ValueError:
926
+ pass
927
+ # END exception handling
928
+ # END for each type to try
929
+ raise ValueError("Could not find reference type suitable to handle path %r" % path)
930
+
931
+ def is_remote(self) -> bool:
932
+ """:return: True if this symbolic reference points to a remote branch"""
933
+ return str(self.path).startswith(self._remote_common_path_default + "/")
.venv/lib/python3.11/site-packages/git/refs/tag.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Provides a :class:`~git.refs.reference.Reference`-based type for lightweight tags.
5
+
6
+ This defines the :class:`TagReference` class (and its alias :class:`Tag`), which
7
+ represents lightweight tags. For annotated tags (which are git objects), see the
8
+ :mod:`git.objects.tag` module.
9
+ """
10
+
11
+ __all__ = ["TagReference", "Tag"]
12
+
13
+ from .reference import Reference
14
+
15
+ # typing ------------------------------------------------------------------
16
+
17
+ from typing import Any, TYPE_CHECKING, Type, Union
18
+
19
+ from git.types import AnyGitObject, PathLike
20
+
21
+ if TYPE_CHECKING:
22
+ from git.objects import Commit, TagObject
23
+ from git.refs import SymbolicReference
24
+ from git.repo import Repo
25
+
26
+ # ------------------------------------------------------------------------------
27
+
28
+
29
+ class TagReference(Reference):
30
+ """A lightweight tag reference which either points to a commit, a tag object or any
31
+ other object. In the latter case additional information, like the signature or the
32
+ tag-creator, is available.
33
+
34
+ This tag object will always point to a commit object, but may carry additional
35
+ information in a tag object::
36
+
37
+ tagref = TagReference.list_items(repo)[0]
38
+ print(tagref.commit.message)
39
+ if tagref.tag is not None:
40
+ print(tagref.tag.message)
41
+ """
42
+
43
+ __slots__ = ()
44
+
45
+ _common_default = "tags"
46
+ _common_path_default = Reference._common_path_default + "/" + _common_default
47
+
48
+ @property
49
+ def commit(self) -> "Commit": # type: ignore[override] # LazyMixin has unrelated commit method
50
+ """:return: Commit object the tag ref points to
51
+
52
+ :raise ValueError:
53
+ If the tag points to a tree or blob.
54
+ """
55
+ obj = self.object
56
+ while obj.type != "commit":
57
+ if obj.type == "tag":
58
+ # It is a tag object which carries the commit as an object - we can point to anything.
59
+ obj = obj.object
60
+ else:
61
+ raise ValueError(
62
+ (
63
+ "Cannot resolve commit as tag %s points to a %s object - "
64
+ + "use the `.object` property instead to access it"
65
+ )
66
+ % (self, obj.type)
67
+ )
68
+ return obj
69
+
70
+ @property
71
+ def tag(self) -> Union["TagObject", None]:
72
+ """
73
+ :return:
74
+ Tag object this tag ref points to, or ``None`` in case we are a lightweight
75
+ tag
76
+ """
77
+ obj = self.object
78
+ if obj.type == "tag":
79
+ return obj
80
+ return None
81
+
82
+ # Make object read-only. It should be reasonably hard to adjust an existing tag.
83
+ @property
84
+ def object(self) -> AnyGitObject: # type: ignore[override]
85
+ return Reference._get_object(self)
86
+
87
+ @classmethod
88
+ def create(
89
+ cls: Type["TagReference"],
90
+ repo: "Repo",
91
+ path: PathLike,
92
+ reference: Union[str, "SymbolicReference"] = "HEAD",
93
+ logmsg: Union[str, None] = None,
94
+ force: bool = False,
95
+ **kwargs: Any,
96
+ ) -> "TagReference":
97
+ """Create a new tag reference.
98
+
99
+ :param repo:
100
+ The :class:`~git.repo.base.Repo` to create the tag in.
101
+
102
+ :param path:
103
+ The name of the tag, e.g. ``1.0`` or ``releases/1.0``.
104
+ The prefix ``refs/tags`` is implied.
105
+
106
+ :param reference:
107
+ A reference to the :class:`~git.objects.base.Object` you want to tag.
108
+ The referenced object can be a commit, tree, or blob.
109
+
110
+ :param logmsg:
111
+ If not ``None``, the message will be used in your tag object. This will also
112
+ create an additional tag object that allows to obtain that information,
113
+ e.g.::
114
+
115
+ tagref.tag.message
116
+
117
+ :param message:
118
+ Synonym for the `logmsg` parameter. Included for backwards compatibility.
119
+ `logmsg` takes precedence if both are passed.
120
+
121
+ :param force:
122
+ If ``True``, force creation of a tag even though that tag already exists.
123
+
124
+ :param kwargs:
125
+ Additional keyword arguments to be passed to :manpage:`git-tag(1)`.
126
+
127
+ :return:
128
+ A new :class:`TagReference`.
129
+ """
130
+ if "ref" in kwargs and kwargs["ref"]:
131
+ reference = kwargs["ref"]
132
+
133
+ if "message" in kwargs and kwargs["message"]:
134
+ kwargs["m"] = kwargs["message"]
135
+ del kwargs["message"]
136
+
137
+ if logmsg:
138
+ kwargs["m"] = logmsg
139
+
140
+ if force:
141
+ kwargs["f"] = True
142
+
143
+ args = (path, reference)
144
+
145
+ repo.git.tag(*args, **kwargs)
146
+ return TagReference(repo, "%s/%s" % (cls._common_path_default, path))
147
+
148
+ @classmethod
149
+ def delete(cls, repo: "Repo", *tags: "TagReference") -> None: # type: ignore[override]
150
+ """Delete the given existing tag or tags."""
151
+ repo.git.tag("-d", *tags)
152
+
153
+
154
+ # Provide an alias.
155
+ Tag = TagReference
.venv/lib/python3.11/site-packages/git/remote.py ADDED
@@ -0,0 +1,1248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Module implementing a remote object allowing easy access to git remotes."""
7
+
8
+ __all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"]
9
+
10
+ import contextlib
11
+ import logging
12
+ import re
13
+
14
+ from git.cmd import Git, handle_process_output
15
+ from git.compat import defenc, force_text
16
+ from git.config import GitConfigParser, SectionConstraint, cp
17
+ from git.exc import GitCommandError
18
+ from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
19
+ from git.util import (
20
+ CallableRemoteProgress,
21
+ IterableList,
22
+ IterableObj,
23
+ LazyMixin,
24
+ RemoteProgress,
25
+ join_path,
26
+ )
27
+
28
+ # typing-------------------------------------------------------
29
+
30
+ from typing import (
31
+ Any,
32
+ Callable,
33
+ Dict,
34
+ Iterator,
35
+ List,
36
+ NoReturn,
37
+ Optional,
38
+ Sequence,
39
+ TYPE_CHECKING,
40
+ Type,
41
+ Union,
42
+ cast,
43
+ overload,
44
+ )
45
+
46
+ from git.types import AnyGitObject, Literal, PathLike
47
+
48
+ if TYPE_CHECKING:
49
+ from git.objects.commit import Commit
50
+ from git.objects.submodule.base import UpdateProgress
51
+ from git.repo.base import Repo
52
+
53
+ flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
54
+
55
+ # -------------------------------------------------------------
56
+
57
+ _logger = logging.getLogger(__name__)
58
+
59
+ # { Utilities
60
+
61
+
62
+ def add_progress(
63
+ kwargs: Any,
64
+ git: Git,
65
+ progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None],
66
+ ) -> Any:
67
+ """Add the ``--progress`` flag to the given `kwargs` dict if supported by the git
68
+ command.
69
+
70
+ :note:
71
+ If the actual progress in the given progress instance is not given, we do not
72
+ request any progress.
73
+
74
+ :return:
75
+ Possibly altered `kwargs`
76
+ """
77
+ if progress is not None:
78
+ v = git.version_info[:2]
79
+ if v >= (1, 7):
80
+ kwargs["progress"] = True
81
+ # END handle --progress
82
+ # END handle progress
83
+ return kwargs
84
+
85
+
86
+ # } END utilities
87
+
88
+
89
+ @overload
90
+ def to_progress_instance(progress: None) -> RemoteProgress: ...
91
+
92
+
93
+ @overload
94
+ def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ...
95
+
96
+
97
+ @overload
98
+ def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ...
99
+
100
+
101
+ def to_progress_instance(
102
+ progress: Union[Callable[..., Any], RemoteProgress, None],
103
+ ) -> Union[RemoteProgress, CallableRemoteProgress]:
104
+ """Given the `progress` return a suitable object derived from
105
+ :class:`~git.util.RemoteProgress`."""
106
+ # New API only needs progress as a function.
107
+ if callable(progress):
108
+ return CallableRemoteProgress(progress)
109
+
110
+ # Where None is passed create a parser that eats the progress.
111
+ elif progress is None:
112
+ return RemoteProgress()
113
+
114
+ # Assume its the old API with an instance of RemoteProgress.
115
+ return progress
116
+
117
+
118
+ class PushInfo(IterableObj):
119
+ """
120
+ Carries information about the result of a push operation of a single head::
121
+
122
+ info = remote.push()[0]
123
+ info.flags # bitflags providing more information about the result
124
+ info.local_ref # Reference pointing to the local reference that was pushed
125
+ # It is None if the ref was deleted.
126
+ info.remote_ref_string # path to the remote reference located on the remote side
127
+ info.remote_ref # Remote Reference on the local side corresponding to
128
+ # the remote_ref_string. It can be a TagReference as well.
129
+ info.old_commit # commit at which the remote_ref was standing before we pushed
130
+ # it to local_ref.commit. Will be None if an error was indicated
131
+ info.summary # summary line providing human readable english text about the push
132
+ """
133
+
134
+ __slots__ = (
135
+ "local_ref",
136
+ "remote_ref_string",
137
+ "flags",
138
+ "_old_commit_sha",
139
+ "_remote",
140
+ "summary",
141
+ )
142
+
143
+ _id_attribute_ = "pushinfo"
144
+
145
+ (
146
+ NEW_TAG,
147
+ NEW_HEAD,
148
+ NO_MATCH,
149
+ REJECTED,
150
+ REMOTE_REJECTED,
151
+ REMOTE_FAILURE,
152
+ DELETED,
153
+ FORCED_UPDATE,
154
+ FAST_FORWARD,
155
+ UP_TO_DATE,
156
+ ERROR,
157
+ ) = [1 << x for x in range(11)]
158
+
159
+ _flag_map = {
160
+ "X": NO_MATCH,
161
+ "-": DELETED,
162
+ "*": 0,
163
+ "+": FORCED_UPDATE,
164
+ " ": FAST_FORWARD,
165
+ "=": UP_TO_DATE,
166
+ "!": ERROR,
167
+ }
168
+
169
+ def __init__(
170
+ self,
171
+ flags: int,
172
+ local_ref: Union[SymbolicReference, None],
173
+ remote_ref_string: str,
174
+ remote: "Remote",
175
+ old_commit: Optional[str] = None,
176
+ summary: str = "",
177
+ ) -> None:
178
+ """Initialize a new instance.
179
+
180
+ local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None
181
+ """
182
+ self.flags = flags
183
+ self.local_ref = local_ref
184
+ self.remote_ref_string = remote_ref_string
185
+ self._remote = remote
186
+ self._old_commit_sha = old_commit
187
+ self.summary = summary
188
+
189
+ @property
190
+ def old_commit(self) -> Union["Commit", None]:
191
+ return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
192
+
193
+ @property
194
+ def remote_ref(self) -> Union[RemoteReference, TagReference]:
195
+ """
196
+ :return:
197
+ Remote :class:`~git.refs.reference.Reference` or
198
+ :class:`~git.refs.tag.TagReference` in the local repository corresponding to
199
+ the :attr:`remote_ref_string` kept in this instance.
200
+ """
201
+ # Translate heads to a local remote. Tags stay as they are.
202
+ if self.remote_ref_string.startswith("refs/tags"):
203
+ return TagReference(self._remote.repo, self.remote_ref_string)
204
+ elif self.remote_ref_string.startswith("refs/heads"):
205
+ remote_ref = Reference(self._remote.repo, self.remote_ref_string)
206
+ return RemoteReference(
207
+ self._remote.repo,
208
+ "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name),
209
+ )
210
+ else:
211
+ raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string)
212
+ # END
213
+
214
+ @classmethod
215
+ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
216
+ """Create a new :class:`PushInfo` instance as parsed from line which is expected
217
+ to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
218
+ control_character, from_to, summary = line.split("\t", 3)
219
+ flags = 0
220
+
221
+ # Control character handling
222
+ try:
223
+ flags |= cls._flag_map[control_character]
224
+ except KeyError as e:
225
+ raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
226
+ # END handle control character
227
+
228
+ # from_to handling
229
+ from_ref_string, to_ref_string = from_to.split(":")
230
+ if flags & cls.DELETED:
231
+ from_ref: Union[SymbolicReference, None] = None
232
+ else:
233
+ if from_ref_string == "(delete)":
234
+ from_ref = None
235
+ else:
236
+ from_ref = Reference.from_path(remote.repo, from_ref_string)
237
+
238
+ # Commit handling, could be message or commit info
239
+ old_commit: Optional[str] = None
240
+ if summary.startswith("["):
241
+ if "[rejected]" in summary:
242
+ flags |= cls.REJECTED
243
+ elif "[remote rejected]" in summary:
244
+ flags |= cls.REMOTE_REJECTED
245
+ elif "[remote failure]" in summary:
246
+ flags |= cls.REMOTE_FAILURE
247
+ elif "[no match]" in summary:
248
+ flags |= cls.ERROR
249
+ elif "[new tag]" in summary:
250
+ flags |= cls.NEW_TAG
251
+ elif "[new branch]" in summary:
252
+ flags |= cls.NEW_HEAD
253
+ # `uptodate` encoded in control character
254
+ else:
255
+ # Fast-forward or forced update - was encoded in control character,
256
+ # but we parse the old and new commit.
257
+ split_token = "..."
258
+ if control_character == " ":
259
+ split_token = ".."
260
+ old_sha, _new_sha = summary.split(" ")[0].split(split_token)
261
+ # Have to use constructor here as the sha usually is abbreviated.
262
+ old_commit = old_sha
263
+ # END message handling
264
+
265
+ return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary)
266
+
267
+ @classmethod
268
+ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['PushInfo']:
269
+ raise NotImplementedError
270
+
271
+
272
+ class PushInfoList(IterableList[PushInfo]):
273
+ """:class:`~git.util.IterableList` of :class:`PushInfo` objects."""
274
+
275
+ def __new__(cls) -> "PushInfoList":
276
+ return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
277
+
278
+ def __init__(self) -> None:
279
+ super().__init__("push_infos")
280
+ self.error: Optional[Exception] = None
281
+
282
+ def raise_if_error(self) -> None:
283
+ """Raise an exception if any ref failed to push."""
284
+ if self.error:
285
+ raise self.error
286
+
287
+
288
+ class FetchInfo(IterableObj):
289
+ """
290
+ Carries information about the results of a fetch operation of a single head::
291
+
292
+ info = remote.fetch()[0]
293
+ info.ref # Symbolic Reference or RemoteReference to the changed
294
+ # remote head or FETCH_HEAD
295
+ info.flags # additional flags to be & with enumeration members,
296
+ # i.e. info.flags & info.REJECTED
297
+ # is 0 if ref is SymbolicReference
298
+ info.note # additional notes given by git-fetch intended for the user
299
+ info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD,
300
+ # field is set to the previous location of ref, otherwise None
301
+ info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref
302
+ """
303
+
304
+ __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path")
305
+
306
+ _id_attribute_ = "fetchinfo"
307
+
308
+ (
309
+ NEW_TAG,
310
+ NEW_HEAD,
311
+ HEAD_UPTODATE,
312
+ TAG_UPDATE,
313
+ REJECTED,
314
+ FORCED_UPDATE,
315
+ FAST_FORWARD,
316
+ ERROR,
317
+ ) = [1 << x for x in range(8)]
318
+
319
+ _re_fetch_result = re.compile(r"^ *(?:.{0,3})(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)( \(.*\)?$)?")
320
+
321
+ _flag_map: Dict[flagKeyLiteral, int] = {
322
+ "!": ERROR,
323
+ "+": FORCED_UPDATE,
324
+ "*": 0,
325
+ "=": HEAD_UPTODATE,
326
+ " ": FAST_FORWARD,
327
+ "-": TAG_UPDATE,
328
+ }
329
+
330
+ @classmethod
331
+ def refresh(cls) -> Literal[True]:
332
+ """Update information about which :manpage:`git-fetch(1)` flags are supported
333
+ by the git executable being used.
334
+
335
+ Called by the :func:`git.refresh` function in the top level ``__init__``.
336
+ """
337
+ # Clear the old values in _flag_map.
338
+ with contextlib.suppress(KeyError):
339
+ del cls._flag_map["t"]
340
+ with contextlib.suppress(KeyError):
341
+ del cls._flag_map["-"]
342
+
343
+ # Set the value given the git version.
344
+ if Git().version_info[:2] >= (2, 10):
345
+ cls._flag_map["t"] = cls.TAG_UPDATE
346
+ else:
347
+ cls._flag_map["-"] = cls.TAG_UPDATE
348
+
349
+ return True
350
+
351
+ def __init__(
352
+ self,
353
+ ref: SymbolicReference,
354
+ flags: int,
355
+ note: str = "",
356
+ old_commit: Union[AnyGitObject, None] = None,
357
+ remote_ref_path: Optional[PathLike] = None,
358
+ ) -> None:
359
+ """Initialize a new instance."""
360
+ self.ref = ref
361
+ self.flags = flags
362
+ self.note = note
363
+ self.old_commit = old_commit
364
+ self.remote_ref_path = remote_ref_path
365
+
366
+ def __str__(self) -> str:
367
+ return self.name
368
+
369
+ @property
370
+ def name(self) -> str:
371
+ """:return: Name of our remote ref"""
372
+ return self.ref.name
373
+
374
+ @property
375
+ def commit(self) -> "Commit":
376
+ """:return: Commit of our remote ref"""
377
+ return self.ref.commit
378
+
379
+ @classmethod
380
+ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
381
+ """Parse information from the given line as returned by ``git-fetch -v`` and
382
+ return a new :class:`FetchInfo` object representing this information.
383
+
384
+ We can handle a line as follows::
385
+
386
+ %c %-*s %-*s -> %s%s
387
+
388
+ Where ``c`` is either a space, ``!``, ``+``, ``-``, ``*``, or ``=``:
389
+
390
+ - '!' means error
391
+ - '+' means success forcing update
392
+ - '-' means a tag was updated
393
+ - '*' means birth of new branch or tag
394
+ - '=' means the head was up to date (and not moved)
395
+ - ' ' means a fast-forward
396
+
397
+ `fetch_line` is the corresponding line from FETCH_HEAD, like::
398
+
399
+ acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
400
+ """
401
+ match = cls._re_fetch_result.match(line)
402
+ if match is None:
403
+ raise ValueError("Failed to parse line: %r" % line)
404
+
405
+ # Parse lines.
406
+ remote_local_ref_str: str
407
+ (
408
+ control_character,
409
+ operation,
410
+ local_remote_ref,
411
+ remote_local_ref_str,
412
+ note,
413
+ ) = match.groups()
414
+ control_character = cast(flagKeyLiteral, control_character)
415
+ try:
416
+ _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t")
417
+ ref_type_name, fetch_note = fetch_note.split(" ", 1)
418
+ except ValueError as e: # unpack error
419
+ raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e
420
+
421
+ # Parse flags from control_character.
422
+ flags = 0
423
+ try:
424
+ flags |= cls._flag_map[control_character]
425
+ except KeyError as e:
426
+ raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
427
+ # END control char exception handling
428
+
429
+ # Parse operation string for more info.
430
+ # This makes no sense for symbolic refs, but we parse it anyway.
431
+ old_commit: Union[AnyGitObject, None] = None
432
+ is_tag_operation = False
433
+ if "rejected" in operation:
434
+ flags |= cls.REJECTED
435
+ if "new tag" in operation:
436
+ flags |= cls.NEW_TAG
437
+ is_tag_operation = True
438
+ if "tag update" in operation:
439
+ flags |= cls.TAG_UPDATE
440
+ is_tag_operation = True
441
+ if "new branch" in operation:
442
+ flags |= cls.NEW_HEAD
443
+ if "..." in operation or ".." in operation:
444
+ split_token = "..."
445
+ if control_character == " ":
446
+ split_token = split_token[:-1]
447
+ old_commit = repo.rev_parse(operation.split(split_token)[0])
448
+ # END handle refspec
449
+
450
+ # Handle FETCH_HEAD and figure out ref type.
451
+ # If we do not specify a target branch like master:refs/remotes/origin/master,
452
+ # the fetch result is stored in FETCH_HEAD which destroys the rule we usually
453
+ # have. In that case we use a symbolic reference which is detached.
454
+ ref_type: Optional[Type[SymbolicReference]] = None
455
+ if remote_local_ref_str == "FETCH_HEAD":
456
+ ref_type = SymbolicReference
457
+ elif ref_type_name == "tag" or is_tag_operation:
458
+ # The ref_type_name can be branch, whereas we are still seeing a tag
459
+ # operation. It happens during testing, which is based on actual git
460
+ # operations.
461
+ ref_type = TagReference
462
+ elif ref_type_name in ("remote-tracking", "branch"):
463
+ # Note: remote-tracking is just the first part of the
464
+ # 'remote-tracking branch' token. We don't parse it correctly, but it's
465
+ # enough to know what to do, and it's new in git 1.7something.
466
+ ref_type = RemoteReference
467
+ elif "/" in ref_type_name:
468
+ # If the fetch spec look something like '+refs/pull/*:refs/heads/pull/*',
469
+ # and is thus pretty much anything the user wants, we will have trouble
470
+ # determining what's going on. For now, we assume the local ref is a Head.
471
+ ref_type = Head
472
+ else:
473
+ raise TypeError("Cannot handle reference type: %r" % ref_type_name)
474
+ # END handle ref type
475
+
476
+ # Create ref instance.
477
+ if ref_type is SymbolicReference:
478
+ remote_local_ref = ref_type(repo, "FETCH_HEAD")
479
+ else:
480
+ # Determine prefix. Tags are usually pulled into refs/tags; they may have
481
+ # subdirectories. It is not clear sometimes where exactly the item is,
482
+ # unless we have an absolute path as indicated by the 'ref/' prefix.
483
+ # Otherwise even a tag could be in refs/remotes, which is when it will have
484
+ # the 'tags/' subdirectory in its path. We don't want to test for actual
485
+ # existence, but try to figure everything out analytically.
486
+ ref_path: Optional[PathLike] = None
487
+ remote_local_ref_str = remote_local_ref_str.strip()
488
+
489
+ if remote_local_ref_str.startswith(Reference._common_path_default + "/"):
490
+ # Always use actual type if we get absolute paths. This will always be
491
+ # the case if something is fetched outside of refs/remotes (if its not a
492
+ # tag).
493
+ ref_path = remote_local_ref_str
494
+ if ref_type is not TagReference and not remote_local_ref_str.startswith(
495
+ RemoteReference._common_path_default + "/"
496
+ ):
497
+ ref_type = Reference
498
+ # END downgrade remote reference
499
+ elif ref_type is TagReference and "tags/" in remote_local_ref_str:
500
+ # Even though it's a tag, it is located in refs/remotes.
501
+ ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str)
502
+ else:
503
+ ref_path = join_path(ref_type._common_path_default, remote_local_ref_str)
504
+ # END obtain refpath
505
+
506
+ # Even though the path could be within the git conventions, we make sure we
507
+ # respect whatever the user wanted, and disabled path checking.
508
+ remote_local_ref = ref_type(repo, ref_path, check_path=False)
509
+ # END create ref instance
510
+
511
+ note = (note and note.strip()) or ""
512
+
513
+ return cls(remote_local_ref, flags, note, old_commit, local_remote_ref)
514
+
515
+ @classmethod
516
+ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['FetchInfo']:
517
+ raise NotImplementedError
518
+
519
+
520
+ class Remote(LazyMixin, IterableObj):
521
+ """Provides easy read and write access to a git remote.
522
+
523
+ Everything not part of this interface is considered an option for the current
524
+ remote, allowing constructs like ``remote.pushurl`` to query the pushurl.
525
+
526
+ :note:
527
+ When querying configuration, the configuration accessor will be cached to speed
528
+ up subsequent accesses.
529
+ """
530
+
531
+ __slots__ = ("repo", "name", "_config_reader")
532
+
533
+ _id_attribute_ = "name"
534
+
535
+ unsafe_git_fetch_options = [
536
+ # This option allows users to execute arbitrary commands.
537
+ # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt
538
+ "--upload-pack",
539
+ ]
540
+ unsafe_git_pull_options = [
541
+ # This option allows users to execute arbitrary commands.
542
+ # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt
543
+ "--upload-pack"
544
+ ]
545
+ unsafe_git_push_options = [
546
+ # This option allows users to execute arbitrary commands.
547
+ # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt
548
+ "--receive-pack",
549
+ "--exec",
550
+ ]
551
+
552
+ url: str # Obtained dynamically from _config_reader. See __getattr__ below.
553
+ """The URL configured for the remote."""
554
+
555
+ def __init__(self, repo: "Repo", name: str) -> None:
556
+ """Initialize a remote instance.
557
+
558
+ :param repo:
559
+ The repository we are a remote of.
560
+
561
+ :param name:
562
+ The name of the remote, e.g. ``origin``.
563
+ """
564
+ self.repo = repo
565
+ self.name = name
566
+
567
+ def __getattr__(self, attr: str) -> Any:
568
+ """Allows to call this instance like ``remote.special(*args, **kwargs)`` to
569
+ call ``git remote special self.name``."""
570
+ if attr == "_config_reader":
571
+ return super().__getattr__(attr)
572
+
573
+ # Sometimes, probably due to a bug in Python itself, we are being called even
574
+ # though a slot of the same name exists.
575
+ try:
576
+ return self._config_reader.get(attr)
577
+ except cp.NoOptionError:
578
+ return super().__getattr__(attr)
579
+ # END handle exception
580
+
581
+ def _config_section_name(self) -> str:
582
+ return 'remote "%s"' % self.name
583
+
584
+ def _set_cache_(self, attr: str) -> None:
585
+ if attr == "_config_reader":
586
+ # NOTE: This is cached as __getattr__ is overridden to return remote config
587
+ # values implicitly, such as in print(r.pushurl).
588
+ self._config_reader = SectionConstraint(
589
+ self.repo.config_reader("repository"),
590
+ self._config_section_name(),
591
+ )
592
+ else:
593
+ super()._set_cache_(attr)
594
+
595
+ def __str__(self) -> str:
596
+ return self.name
597
+
598
+ def __repr__(self) -> str:
599
+ return '<git.%s "%s">' % (self.__class__.__name__, self.name)
600
+
601
+ def __eq__(self, other: object) -> bool:
602
+ return isinstance(other, type(self)) and self.name == other.name
603
+
604
+ def __ne__(self, other: object) -> bool:
605
+ return not (self == other)
606
+
607
+ def __hash__(self) -> int:
608
+ return hash(self.name)
609
+
610
+ def exists(self) -> bool:
611
+ """
612
+ :return:
613
+ ``True`` if this is a valid, existing remote.
614
+ Valid remotes have an entry in the repository's configuration.
615
+ """
616
+ try:
617
+ self.config_reader.get("url")
618
+ return True
619
+ except cp.NoOptionError:
620
+ # We have the section at least...
621
+ return True
622
+ except cp.NoSectionError:
623
+ return False
624
+
625
+ @classmethod
626
+ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]:
627
+ """:return: Iterator yielding :class:`Remote` objects of the given repository"""
628
+ for section in repo.config_reader("repository").sections():
629
+ if not section.startswith("remote "):
630
+ continue
631
+ lbound = section.find('"')
632
+ rbound = section.rfind('"')
633
+ if lbound == -1 or rbound == -1:
634
+ raise ValueError("Remote-Section has invalid format: %r" % section)
635
+ yield Remote(repo, section[lbound + 1 : rbound])
636
+ # END for each configuration section
637
+
638
+ def set_url(
639
+ self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
640
+ ) -> "Remote":
641
+ """Configure URLs on current remote (cf. command ``git remote set-url``).
642
+
643
+ This command manages URLs on the remote.
644
+
645
+ :param new_url:
646
+ String being the URL to add as an extra remote URL.
647
+
648
+ :param old_url:
649
+ When set, replaces this URL with `new_url` for the remote.
650
+
651
+ :param allow_unsafe_protocols:
652
+ Allow unsafe protocols to be used, like ``ext``.
653
+
654
+ :return:
655
+ self
656
+ """
657
+ if not allow_unsafe_protocols:
658
+ Git.check_unsafe_protocols(new_url)
659
+ scmd = "set-url"
660
+ kwargs["insert_kwargs_after"] = scmd
661
+ if old_url:
662
+ self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs)
663
+ else:
664
+ self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs)
665
+ return self
666
+
667
+ def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
668
+ """Adds a new url on current remote (special case of ``git remote set-url``).
669
+
670
+ This command adds new URLs to a given remote, making it possible to have
671
+ multiple URLs for a single remote.
672
+
673
+ :param url:
674
+ String being the URL to add as an extra remote URL.
675
+
676
+ :param allow_unsafe_protocols:
677
+ Allow unsafe protocols to be used, like ``ext``.
678
+
679
+ :return:
680
+ self
681
+ """
682
+ return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
683
+
684
+ def delete_url(self, url: str, **kwargs: Any) -> "Remote":
685
+ """Deletes a new url on current remote (special case of ``git remote set-url``).
686
+
687
+ This command deletes new URLs to a given remote, making it possible to have
688
+ multiple URLs for a single remote.
689
+
690
+ :param url:
691
+ String being the URL to delete from the remote.
692
+
693
+ :return:
694
+ self
695
+ """
696
+ return self.set_url(url, delete=True)
697
+
698
+ @property
699
+ def urls(self) -> Iterator[str]:
700
+ """:return: Iterator yielding all configured URL targets on a remote as strings"""
701
+ try:
702
+ remote_details = self.repo.git.remote("get-url", "--all", self.name)
703
+ assert isinstance(remote_details, str)
704
+ for line in remote_details.split("\n"):
705
+ yield line
706
+ except GitCommandError as ex:
707
+ ## We are on git < 2.7 (i.e TravisCI as of Oct-2016),
708
+ # so `get-utl` command does not exist yet!
709
+ # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319
710
+ # and: http://stackoverflow.com/a/32991784/548792
711
+ #
712
+ if "Unknown subcommand: get-url" in str(ex):
713
+ try:
714
+ remote_details = self.repo.git.remote("show", self.name)
715
+ assert isinstance(remote_details, str)
716
+ for line in remote_details.split("\n"):
717
+ if " Push URL:" in line:
718
+ yield line.split(": ")[-1]
719
+ except GitCommandError as _ex:
720
+ if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]):
721
+ # If ssh is not setup to access this repository, see issue 694.
722
+ remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name)
723
+ assert isinstance(remote_details, str)
724
+ for line in remote_details.split("\n"):
725
+ yield line
726
+ else:
727
+ raise _ex
728
+ else:
729
+ raise ex
730
+
731
+ @property
732
+ def refs(self) -> IterableList[RemoteReference]:
733
+ """
734
+ :return:
735
+ :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
736
+ objects.
737
+
738
+ It is prefixed, allowing you to omit the remote path portion, e.g.::
739
+
740
+ remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')
741
+ """
742
+ out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
743
+ out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name))
744
+ return out_refs
745
+
746
+ @property
747
+ def stale_refs(self) -> IterableList[Reference]:
748
+ """
749
+ :return:
750
+ :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
751
+ objects that do not have a corresponding head in the remote reference
752
+ anymore as they have been deleted on the remote side, but are still
753
+ available locally.
754
+
755
+ The :class:`~git.util.IterableList` is prefixed, hence the 'origin' must be
756
+ omitted. See :attr:`refs` property for an example.
757
+
758
+ To make things more complicated, it can be possible for the list to include
759
+ other kinds of references, for example, tag references, if these are stale
760
+ as well. This is a fix for the issue described here:
761
+ https://github.com/gitpython-developers/GitPython/issues/260
762
+ """
763
+ out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
764
+ for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]:
765
+ # expecting
766
+ # * [would prune] origin/new_branch
767
+ token = " * [would prune] "
768
+ if not line.startswith(token):
769
+ continue
770
+ ref_name = line.replace(token, "")
771
+ # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260.
772
+ if ref_name.startswith(Reference._common_path_default + "/"):
773
+ out_refs.append(Reference.from_path(self.repo, ref_name))
774
+ else:
775
+ fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name)
776
+ out_refs.append(RemoteReference(self.repo, fqhn))
777
+ # END special case handling
778
+ # END for each line
779
+ return out_refs
780
+
781
+ @classmethod
782
+ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
783
+ """Create a new remote to the given repository.
784
+
785
+ :param repo:
786
+ Repository instance that is to receive the new remote.
787
+
788
+ :param name:
789
+ Desired name of the remote.
790
+
791
+ :param url:
792
+ URL which corresponds to the remote's name.
793
+
794
+ :param allow_unsafe_protocols:
795
+ Allow unsafe protocols to be used, like ``ext``.
796
+
797
+ :param kwargs:
798
+ Additional arguments to be passed to the ``git remote add`` command.
799
+
800
+ :return:
801
+ New :class:`Remote` instance
802
+
803
+ :raise git.exc.GitCommandError:
804
+ In case an origin with that name already exists.
805
+ """
806
+ scmd = "add"
807
+ kwargs["insert_kwargs_after"] = scmd
808
+ url = Git.polish_url(url)
809
+ if not allow_unsafe_protocols:
810
+ Git.check_unsafe_protocols(url)
811
+ repo.git.remote(scmd, "--", name, url, **kwargs)
812
+ return cls(repo, name)
813
+
814
+ # `add` is an alias.
815
+ @classmethod
816
+ def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
817
+ return cls.create(repo, name, url, **kwargs)
818
+
819
+ @classmethod
820
+ def remove(cls, repo: "Repo", name: str) -> str:
821
+ """Remove the remote with the given name.
822
+
823
+ :return:
824
+ The passed remote name to remove
825
+ """
826
+ repo.git.remote("rm", name)
827
+ if isinstance(name, cls):
828
+ name._clear_cache()
829
+ return name
830
+
831
+ @classmethod
832
+ def rm(cls, repo: "Repo", name: str) -> str:
833
+ """Alias of remove.
834
+ Remove the remote with the given name.
835
+
836
+ :return:
837
+ The passed remote name to remove
838
+ """
839
+ return cls.remove(repo, name)
840
+
841
+ def rename(self, new_name: str) -> "Remote":
842
+ """Rename self to the given `new_name`.
843
+
844
+ :return:
845
+ self
846
+ """
847
+ if self.name == new_name:
848
+ return self
849
+
850
+ self.repo.git.remote("rename", self.name, new_name)
851
+ self.name = new_name
852
+ self._clear_cache()
853
+
854
+ return self
855
+
856
+ def update(self, **kwargs: Any) -> "Remote":
857
+ """Fetch all changes for this remote, including new branches which will be
858
+ forced in (in case your local remote branch is not part the new remote branch's
859
+ ancestry anymore).
860
+
861
+ :param kwargs:
862
+ Additional arguments passed to ``git remote update``.
863
+
864
+ :return:
865
+ self
866
+ """
867
+ scmd = "update"
868
+ kwargs["insert_kwargs_after"] = scmd
869
+ self.repo.git.remote(scmd, self.name, **kwargs)
870
+ return self
871
+
872
+ def _get_fetch_info_from_stderr(
873
+ self,
874
+ proc: "Git.AutoInterrupt",
875
+ progress: Union[Callable[..., Any], RemoteProgress, None],
876
+ kill_after_timeout: Union[None, float] = None,
877
+ ) -> IterableList["FetchInfo"]:
878
+ progress = to_progress_instance(progress)
879
+
880
+ # Skip first line as it is some remote info we are not interested in.
881
+ output: IterableList["FetchInfo"] = IterableList("name")
882
+
883
+ # Lines which are no progress are fetch info lines.
884
+ # This also waits for the command to finish.
885
+ # Skip some progress lines that don't provide relevant information.
886
+ fetch_info_lines = []
887
+ # Basically we want all fetch info lines which appear to be in regular form, and
888
+ # thus have a command character. Everything else we ignore.
889
+ cmds = set(FetchInfo._flag_map.keys())
890
+
891
+ progress_handler = progress.new_message_handler()
892
+ handle_process_output(
893
+ proc,
894
+ None,
895
+ progress_handler,
896
+ finalizer=None,
897
+ decode_streams=False,
898
+ kill_after_timeout=kill_after_timeout,
899
+ )
900
+
901
+ stderr_text = progress.error_lines and "\n".join(progress.error_lines) or ""
902
+ proc.wait(stderr=stderr_text)
903
+ if stderr_text:
904
+ _logger.warning("Error lines received while fetching: %s", stderr_text)
905
+
906
+ for line in progress.other_lines:
907
+ line = force_text(line)
908
+ for cmd in cmds:
909
+ if len(line) > 1 and line[0] == " " and line[1] == cmd:
910
+ fetch_info_lines.append(line)
911
+ continue
912
+
913
+ # Read head information.
914
+ fetch_head = SymbolicReference(self.repo, "FETCH_HEAD")
915
+ with open(fetch_head.abspath, "rb") as fp:
916
+ fetch_head_info = [line.decode(defenc) for line in fp.readlines()]
917
+
918
+ l_fil = len(fetch_info_lines)
919
+ l_fhi = len(fetch_head_info)
920
+ if l_fil != l_fhi:
921
+ msg = "Fetch head lines do not match lines provided via progress information\n"
922
+ msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n"
923
+ msg += "Will ignore extra progress lines or fetch head lines."
924
+ msg %= (l_fil, l_fhi)
925
+ _logger.debug(msg)
926
+ _logger.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8"))
927
+ _logger.debug(b"head info: " + str(fetch_head_info).encode("UTF-8"))
928
+ if l_fil < l_fhi:
929
+ fetch_head_info = fetch_head_info[:l_fil]
930
+ else:
931
+ fetch_info_lines = fetch_info_lines[:l_fhi]
932
+ # END truncate correct list
933
+ # END sanity check + sanitization
934
+
935
+ for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info):
936
+ try:
937
+ output.append(FetchInfo._from_line(self.repo, err_line, fetch_line))
938
+ except ValueError as exc:
939
+ _logger.debug("Caught error while parsing line: %s", exc)
940
+ _logger.warning("Git informed while fetching: %s", err_line.strip())
941
+ return output
942
+
943
+ def _get_push_info(
944
+ self,
945
+ proc: "Git.AutoInterrupt",
946
+ progress: Union[Callable[..., Any], RemoteProgress, None],
947
+ kill_after_timeout: Union[None, float] = None,
948
+ ) -> PushInfoList:
949
+ progress = to_progress_instance(progress)
950
+
951
+ # Read progress information from stderr.
952
+ # We hope stdout can hold all the data, it should...
953
+ # Read the lines manually as it will use carriage returns between the messages
954
+ # to override the previous one. This is why we read the bytes manually.
955
+ progress_handler = progress.new_message_handler()
956
+ output: PushInfoList = PushInfoList()
957
+
958
+ def stdout_handler(line: str) -> None:
959
+ try:
960
+ output.append(PushInfo._from_line(self, line))
961
+ except ValueError:
962
+ # If an error happens, additional info is given which we parse below.
963
+ pass
964
+
965
+ handle_process_output(
966
+ proc,
967
+ stdout_handler,
968
+ progress_handler,
969
+ finalizer=None,
970
+ decode_streams=False,
971
+ kill_after_timeout=kill_after_timeout,
972
+ )
973
+ stderr_text = progress.error_lines and "\n".join(progress.error_lines) or ""
974
+ try:
975
+ proc.wait(stderr=stderr_text)
976
+ except Exception as e:
977
+ # This is different than fetch (which fails if there is any stderr
978
+ # even if there is an output).
979
+ if not output:
980
+ raise
981
+ elif stderr_text:
982
+ _logger.warning("Error lines received while fetching: %s", stderr_text)
983
+ output.error = e
984
+
985
+ return output
986
+
987
+ def _assert_refspec(self) -> None:
988
+ """Turns out we can't deal with remotes if the refspec is missing."""
989
+ config = self.config_reader
990
+ unset = "placeholder"
991
+ try:
992
+ if config.get_value("fetch", default=unset) is unset:
993
+ msg = "Remote '%s' has no refspec set.\n"
994
+ msg += "You can set it as follows:"
995
+ msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'."
996
+ raise AssertionError(msg % (self.name, self.name))
997
+ finally:
998
+ config.release()
999
+
1000
+ def fetch(
1001
+ self,
1002
+ refspec: Union[str, List[str], None] = None,
1003
+ progress: Union[RemoteProgress, None, "UpdateProgress"] = None,
1004
+ verbose: bool = True,
1005
+ kill_after_timeout: Union[None, float] = None,
1006
+ allow_unsafe_protocols: bool = False,
1007
+ allow_unsafe_options: bool = False,
1008
+ **kwargs: Any,
1009
+ ) -> IterableList[FetchInfo]:
1010
+ """Fetch the latest changes for this remote.
1011
+
1012
+ :param refspec:
1013
+ A "refspec" is used by fetch and push to describe the mapping
1014
+ between remote ref and local ref. They are combined with a colon in
1015
+ the format ``<src>:<dst>``, preceded by an optional plus sign, ``+``.
1016
+ For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means
1017
+ "grab the master branch head from the $URL and store it as my origin
1018
+ branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream``
1019
+ means "publish my master branch head as to-upstream branch at $URL".
1020
+ See also :manpage:`git-push(1)`.
1021
+
1022
+ Taken from the git manual, :manpage:`gitglossary(7)`.
1023
+
1024
+ Fetch supports multiple refspecs (as the underlying :manpage:`git-fetch(1)`
1025
+ does) - supplying a list rather than a string for 'refspec' will make use of
1026
+ this facility.
1027
+
1028
+ :param progress:
1029
+ See the :meth:`push` method.
1030
+
1031
+ :param verbose:
1032
+ Boolean for verbose output.
1033
+
1034
+ :param kill_after_timeout:
1035
+ To specify a timeout in seconds for the git command, after which the process
1036
+ should be killed. It is set to ``None`` by default.
1037
+
1038
+ :param allow_unsafe_protocols:
1039
+ Allow unsafe protocols to be used, like ``ext``.
1040
+
1041
+ :param allow_unsafe_options:
1042
+ Allow unsafe options to be used, like ``--upload-pack``.
1043
+
1044
+ :param kwargs:
1045
+ Additional arguments to be passed to :manpage:`git-fetch(1)`.
1046
+
1047
+ :return:
1048
+ IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing
1049
+ detailed information about the fetch results
1050
+
1051
+ :note:
1052
+ As fetch does not provide progress information to non-ttys, we cannot make
1053
+ it available here unfortunately as in the :meth:`push` method.
1054
+ """
1055
+ if refspec is None:
1056
+ # No argument refspec, then ensure the repo's config has a fetch refspec.
1057
+ self._assert_refspec()
1058
+
1059
+ kwargs = add_progress(kwargs, self.repo.git, progress)
1060
+ if isinstance(refspec, list):
1061
+ args: Sequence[Optional[str]] = refspec
1062
+ else:
1063
+ args = [refspec]
1064
+
1065
+ if not allow_unsafe_protocols:
1066
+ for ref in args:
1067
+ if ref:
1068
+ Git.check_unsafe_protocols(ref)
1069
+
1070
+ if not allow_unsafe_options:
1071
+ Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
1072
+
1073
+ proc = self.repo.git.fetch(
1074
+ "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
1075
+ )
1076
+ res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
1077
+ if hasattr(self.repo.odb, "update_cache"):
1078
+ self.repo.odb.update_cache()
1079
+ return res
1080
+
1081
+ def pull(
1082
+ self,
1083
+ refspec: Union[str, List[str], None] = None,
1084
+ progress: Union[RemoteProgress, "UpdateProgress", None] = None,
1085
+ kill_after_timeout: Union[None, float] = None,
1086
+ allow_unsafe_protocols: bool = False,
1087
+ allow_unsafe_options: bool = False,
1088
+ **kwargs: Any,
1089
+ ) -> IterableList[FetchInfo]:
1090
+ """Pull changes from the given branch, being the same as a fetch followed by a
1091
+ merge of branch with your local branch.
1092
+
1093
+ :param refspec:
1094
+ See :meth:`fetch` method.
1095
+
1096
+ :param progress:
1097
+ See :meth:`push` method.
1098
+
1099
+ :param kill_after_timeout:
1100
+ See :meth:`fetch` method.
1101
+
1102
+ :param allow_unsafe_protocols:
1103
+ Allow unsafe protocols to be used, like ``ext``.
1104
+
1105
+ :param allow_unsafe_options:
1106
+ Allow unsafe options to be used, like ``--upload-pack``.
1107
+
1108
+ :param kwargs:
1109
+ Additional arguments to be passed to :manpage:`git-pull(1)`.
1110
+
1111
+ :return:
1112
+ Please see :meth:`fetch` method.
1113
+ """
1114
+ if refspec is None:
1115
+ # No argument refspec, then ensure the repo's config has a fetch refspec.
1116
+ self._assert_refspec()
1117
+ kwargs = add_progress(kwargs, self.repo.git, progress)
1118
+
1119
+ refspec = Git._unpack_args(refspec or [])
1120
+ if not allow_unsafe_protocols:
1121
+ for ref in refspec:
1122
+ Git.check_unsafe_protocols(ref)
1123
+
1124
+ if not allow_unsafe_options:
1125
+ Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
1126
+
1127
+ proc = self.repo.git.pull(
1128
+ "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
1129
+ )
1130
+ res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
1131
+ if hasattr(self.repo.odb, "update_cache"):
1132
+ self.repo.odb.update_cache()
1133
+ return res
1134
+
1135
+ def push(
1136
+ self,
1137
+ refspec: Union[str, List[str], None] = None,
1138
+ progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None,
1139
+ kill_after_timeout: Union[None, float] = None,
1140
+ allow_unsafe_protocols: bool = False,
1141
+ allow_unsafe_options: bool = False,
1142
+ **kwargs: Any,
1143
+ ) -> PushInfoList:
1144
+ """Push changes from source branch in refspec to target branch in refspec.
1145
+
1146
+ :param refspec:
1147
+ See :meth:`fetch` method.
1148
+
1149
+ :param progress:
1150
+ Can take one of many value types:
1151
+
1152
+ * ``None``, to discard progress information.
1153
+ * A function (callable) that is called with the progress information.
1154
+ Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
1155
+ See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a
1156
+ description of all arguments given to the function.
1157
+ * An instance of a class derived from :class:`~git.util.RemoteProgress` that
1158
+ overrides the
1159
+ :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method.
1160
+
1161
+ :note:
1162
+ No further progress information is returned after push returns.
1163
+
1164
+ :param kill_after_timeout:
1165
+ To specify a timeout in seconds for the git command, after which the process
1166
+ should be killed. It is set to ``None`` by default.
1167
+
1168
+ :param allow_unsafe_protocols:
1169
+ Allow unsafe protocols to be used, like ``ext``.
1170
+
1171
+ :param allow_unsafe_options:
1172
+ Allow unsafe options to be used, like ``--receive-pack``.
1173
+
1174
+ :param kwargs:
1175
+ Additional arguments to be passed to :manpage:`git-push(1)`.
1176
+
1177
+ :return:
1178
+ A :class:`PushInfoList` object, where each list member represents an
1179
+ individual head which had been updated on the remote side.
1180
+
1181
+ If the push contains rejected heads, these will have the
1182
+ :const:`PushInfo.ERROR` bit set in their flags.
1183
+
1184
+ If the operation fails completely, the length of the returned
1185
+ :class:`PushInfoList` will be 0.
1186
+
1187
+ Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on
1188
+ any failure.
1189
+ """
1190
+ kwargs = add_progress(kwargs, self.repo.git, progress)
1191
+
1192
+ refspec = Git._unpack_args(refspec or [])
1193
+ if not allow_unsafe_protocols:
1194
+ for ref in refspec:
1195
+ Git.check_unsafe_protocols(ref)
1196
+
1197
+ if not allow_unsafe_options:
1198
+ Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)
1199
+
1200
+ proc = self.repo.git.push(
1201
+ "--",
1202
+ self,
1203
+ refspec,
1204
+ porcelain=True,
1205
+ as_process=True,
1206
+ universal_newlines=True,
1207
+ kill_after_timeout=kill_after_timeout,
1208
+ **kwargs,
1209
+ )
1210
+ return self._get_push_info(proc, progress, kill_after_timeout=kill_after_timeout)
1211
+
1212
+ @property
1213
+ def config_reader(self) -> SectionConstraint[GitConfigParser]:
1214
+ """
1215
+ :return:
1216
+ :class:`~git.config.GitConfigParser` compatible object able to read options
1217
+ for only our remote. Hence you may simply type ``config.get("pushurl")`` to
1218
+ obtain the information.
1219
+ """
1220
+ return self._config_reader
1221
+
1222
+ def _clear_cache(self) -> None:
1223
+ try:
1224
+ del self._config_reader
1225
+ except AttributeError:
1226
+ pass
1227
+ # END handle exception
1228
+
1229
+ @property
1230
+ def config_writer(self) -> SectionConstraint:
1231
+ """
1232
+ :return:
1233
+ :class:`~git.config.GitConfigParser`-compatible object able to write options
1234
+ for this remote.
1235
+
1236
+ :note:
1237
+ You can only own one writer at a time - delete it to release the
1238
+ configuration file and make it usable by others.
1239
+
1240
+ To assure consistent results, you should only query options through the
1241
+ writer. Once you are done writing, you are free to use the config reader
1242
+ once again.
1243
+ """
1244
+ writer = self.repo.config_writer()
1245
+
1246
+ # Clear our cache to ensure we re-read the possibly changed configuration.
1247
+ self._clear_cache()
1248
+ return SectionConstraint(writer, self._config_section_name())
.venv/lib/python3.11/site-packages/git/repo/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Initialize the repo package."""
5
+
6
+ __all__ = ["Repo"]
7
+
8
+ from .base import Repo
.venv/lib/python3.11/site-packages/git/repo/base.py ADDED
@@ -0,0 +1,1638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ from __future__ import annotations
7
+
8
+ __all__ = ["Repo"]
9
+
10
+ import gc
11
+ import logging
12
+ import os
13
+ import os.path as osp
14
+ from pathlib import Path
15
+ import re
16
+ import shlex
17
+ import sys
18
+ import warnings
19
+
20
+ import gitdb
21
+ from gitdb.db.loose import LooseObjectDB
22
+ from gitdb.exc import BadObject
23
+
24
+ from git.cmd import Git, handle_process_output
25
+ from git.compat import defenc, safe_decode
26
+ from git.config import GitConfigParser
27
+ from git.db import GitCmdObjectDB
28
+ from git.exc import (
29
+ GitCommandError,
30
+ InvalidGitRepositoryError,
31
+ NoSuchPathError,
32
+ )
33
+ from git.index import IndexFile
34
+ from git.objects import Submodule, RootModule, Commit
35
+ from git.refs import HEAD, Head, Reference, TagReference
36
+ from git.remote import Remote, add_progress, to_progress_instance
37
+ from git.util import (
38
+ Actor,
39
+ cygpath,
40
+ expand_path,
41
+ finalize_process,
42
+ hex_to_bin,
43
+ remove_password_if_present,
44
+ )
45
+
46
+ from .fun import (
47
+ find_submodule_git_dir,
48
+ find_worktree_git_dir,
49
+ is_git_dir,
50
+ rev_parse,
51
+ touch,
52
+ )
53
+
54
+ # typing ------------------------------------------------------
55
+
56
+ from git.types import (
57
+ CallableProgress,
58
+ Commit_ish,
59
+ Lit_config_levels,
60
+ PathLike,
61
+ TBD,
62
+ Tree_ish,
63
+ assert_never,
64
+ )
65
+ from typing import (
66
+ Any,
67
+ BinaryIO,
68
+ Callable,
69
+ Dict,
70
+ Iterator,
71
+ List,
72
+ Mapping,
73
+ NamedTuple,
74
+ Optional,
75
+ Sequence,
76
+ TYPE_CHECKING,
77
+ TextIO,
78
+ Tuple,
79
+ Type,
80
+ Union,
81
+ cast,
82
+ )
83
+
84
+ from git.types import ConfigLevels_Tup, TypedDict
85
+
86
+ if TYPE_CHECKING:
87
+ from git.objects import Tree
88
+ from git.objects.submodule.base import UpdateProgress
89
+ from git.refs.symbolic import SymbolicReference
90
+ from git.remote import RemoteProgress
91
+ from git.util import IterableList
92
+
93
+ # -----------------------------------------------------------
94
+
95
+ _logger = logging.getLogger(__name__)
96
+
97
+
98
+ class BlameEntry(NamedTuple):
99
+ commit: Dict[str, Commit]
100
+ linenos: range
101
+ orig_path: Optional[str]
102
+ orig_linenos: range
103
+
104
+
105
+ class Repo:
106
+ """Represents a git repository and allows you to query references, create commit
107
+ information, generate diffs, create and clone repositories, and query the log.
108
+
109
+ The following attributes are worth using:
110
+
111
+ * :attr:`working_dir` is the working directory of the git command, which is the
112
+ working tree directory if available or the ``.git`` directory in case of bare
113
+ repositories.
114
+
115
+ * :attr:`working_tree_dir` is the working tree directory, but will return ``None``
116
+ if we are a bare repository.
117
+
118
+ * :attr:`git_dir` is the ``.git`` repository directory, which is always set.
119
+ """
120
+
121
+ DAEMON_EXPORT_FILE = "git-daemon-export-ok"
122
+
123
+ # Must exist, or __del__ will fail in case we raise on `__init__()`.
124
+ git = cast("Git", None)
125
+
126
+ working_dir: PathLike
127
+ """The working directory of the git command."""
128
+
129
+ _working_tree_dir: Optional[PathLike] = None
130
+
131
+ git_dir: PathLike
132
+ """The ``.git`` repository directory."""
133
+
134
+ _common_dir: PathLike = ""
135
+
136
+ # Precompiled regex
137
+ re_whitespace = re.compile(r"\s+")
138
+ re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
139
+ re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$")
140
+ re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
141
+ re_author_committer_start = re.compile(r"^(author|committer)")
142
+ re_tab_full_line = re.compile(r"^\t(.*)$")
143
+
144
+ unsafe_git_clone_options = [
145
+ # Executes arbitrary commands:
146
+ "--upload-pack",
147
+ "-u",
148
+ # Can override configuration variables that execute arbitrary commands:
149
+ "--config",
150
+ "-c",
151
+ ]
152
+ """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
153
+
154
+ The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
155
+ directly:
156
+ https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
157
+
158
+ The ``--config``/``-c`` option allows users to override configuration variables like
159
+ ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands:
160
+ https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
161
+ """
162
+
163
+ # Invariants
164
+ config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
165
+ """Represents the configuration level of a configuration file."""
166
+
167
+ # Subclass configuration
168
+ GitCommandWrapperType = Git
169
+ """Subclasses may easily bring in their own custom types by placing a constructor or
170
+ type here."""
171
+
172
+ def __init__(
173
+ self,
174
+ path: Optional[PathLike] = None,
175
+ odbt: Type[LooseObjectDB] = GitCmdObjectDB,
176
+ search_parent_directories: bool = False,
177
+ expand_vars: bool = True,
178
+ ) -> None:
179
+ R"""Create a new :class:`Repo` instance.
180
+
181
+ :param path:
182
+ The path to either the worktree directory or the .git directory itself::
183
+
184
+ repo = Repo("/Users/mtrier/Development/git-python")
185
+ repo = Repo("/Users/mtrier/Development/git-python.git")
186
+ repo = Repo("~/Development/git-python.git")
187
+ repo = Repo("$REPOSITORIES/Development/git-python.git")
188
+ repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
189
+
190
+ - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
191
+ - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If
192
+ that environment variable is absent or empty, the current directory is
193
+ used.
194
+
195
+ :param odbt:
196
+ Object DataBase type - a type which is constructed by providing the
197
+ directory containing the database objects, i.e. ``.git/objects``. It will be
198
+ used to access all object data.
199
+
200
+ :param search_parent_directories:
201
+ If ``True``, all parent directories will be searched for a valid repo as
202
+ well.
203
+
204
+ Please note that this was the default behaviour in older versions of
205
+ GitPython, which is considered a bug though.
206
+
207
+ :raise git.exc.InvalidGitRepositoryError:
208
+
209
+ :raise git.exc.NoSuchPathError:
210
+
211
+ :return:
212
+ :class:`Repo`
213
+ """
214
+
215
+ epath = path or os.getenv("GIT_DIR")
216
+ if not epath:
217
+ epath = os.getcwd()
218
+ if Git.is_cygwin():
219
+ # Given how the tests are written, this seems more likely to catch Cygwin
220
+ # git used from Windows than Windows git used from Cygwin. Therefore
221
+ # changing to Cygwin-style paths is the relevant operation.
222
+ epath = cygpath(str(epath))
223
+
224
+ epath = epath or path or os.getcwd()
225
+ if not isinstance(epath, str):
226
+ epath = str(epath)
227
+ if expand_vars and re.search(self.re_envvars, epath):
228
+ warnings.warn(
229
+ "The use of environment variables in paths is deprecated"
230
+ + "\nfor security reasons and may be removed in the future!!",
231
+ stacklevel=1,
232
+ )
233
+ epath = expand_path(epath, expand_vars)
234
+ if epath is not None:
235
+ if not os.path.exists(epath):
236
+ raise NoSuchPathError(epath)
237
+
238
+ # Walk up the path to find the `.git` dir.
239
+ curpath = epath
240
+ git_dir = None
241
+ while curpath:
242
+ # ABOUT osp.NORMPATH
243
+ # It's important to normalize the paths, as submodules will otherwise
244
+ # initialize their repo instances with paths that depend on path-portions
245
+ # that will not exist after being removed. It's just cleaner.
246
+ if is_git_dir(curpath):
247
+ git_dir = curpath
248
+ # from man git-config : core.worktree
249
+ # Set the path to the root of the working tree. If GIT_COMMON_DIR
250
+ # environment variable is set, core.worktree is ignored and not used for
251
+ # determining the root of working tree. This can be overridden by the
252
+ # GIT_WORK_TREE environment variable. The value can be an absolute path
253
+ # or relative to the path to the .git directory, which is either
254
+ # specified by GIT_DIR, or automatically discovered. If GIT_DIR is
255
+ # specified but none of GIT_WORK_TREE and core.worktree is specified,
256
+ # the current working directory is regarded as the top level of your
257
+ # working tree.
258
+ self._working_tree_dir = os.path.dirname(git_dir)
259
+ if os.environ.get("GIT_COMMON_DIR") is None:
260
+ gitconf = self._config_reader("repository", git_dir)
261
+ if gitconf.has_option("core", "worktree"):
262
+ self._working_tree_dir = gitconf.get("core", "worktree")
263
+ if "GIT_WORK_TREE" in os.environ:
264
+ self._working_tree_dir = os.getenv("GIT_WORK_TREE")
265
+ break
266
+
267
+ dotgit = osp.join(curpath, ".git")
268
+ sm_gitpath = find_submodule_git_dir(dotgit)
269
+ if sm_gitpath is not None:
270
+ git_dir = osp.normpath(sm_gitpath)
271
+
272
+ sm_gitpath = find_submodule_git_dir(dotgit)
273
+ if sm_gitpath is None:
274
+ sm_gitpath = find_worktree_git_dir(dotgit)
275
+
276
+ if sm_gitpath is not None:
277
+ git_dir = expand_path(sm_gitpath, expand_vars)
278
+ self._working_tree_dir = curpath
279
+ break
280
+
281
+ if not search_parent_directories:
282
+ break
283
+ curpath, tail = osp.split(curpath)
284
+ if not tail:
285
+ break
286
+ # END while curpath
287
+
288
+ if git_dir is None:
289
+ raise InvalidGitRepositoryError(epath)
290
+ self.git_dir = git_dir
291
+
292
+ self._bare = False
293
+ try:
294
+ self._bare = self.config_reader("repository").getboolean("core", "bare")
295
+ except Exception:
296
+ # Let's not assume the option exists, although it should.
297
+ pass
298
+
299
+ try:
300
+ common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
301
+ self._common_dir = osp.join(self.git_dir, common_dir)
302
+ except OSError:
303
+ self._common_dir = ""
304
+
305
+ # Adjust the working directory in case we are actually bare - we didn't know
306
+ # that in the first place.
307
+ if self._bare:
308
+ self._working_tree_dir = None
309
+ # END working dir handling
310
+
311
+ self.working_dir: PathLike = self._working_tree_dir or self.common_dir
312
+ self.git = self.GitCommandWrapperType(self.working_dir)
313
+
314
+ # Special handling, in special times.
315
+ rootpath = osp.join(self.common_dir, "objects")
316
+ if issubclass(odbt, GitCmdObjectDB):
317
+ self.odb = odbt(rootpath, self.git)
318
+ else:
319
+ self.odb = odbt(rootpath)
320
+
321
+ def __enter__(self) -> "Repo":
322
+ return self
323
+
324
+ def __exit__(self, *args: Any) -> None:
325
+ self.close()
326
+
327
+ def __del__(self) -> None:
328
+ try:
329
+ self.close()
330
+ except Exception:
331
+ pass
332
+
333
+ def close(self) -> None:
334
+ if self.git:
335
+ self.git.clear_cache()
336
+ # Tempfiles objects on Windows are holding references to open files until
337
+ # they are collected by the garbage collector, thus preventing deletion.
338
+ # TODO: Find these references and ensure they are closed and deleted
339
+ # synchronously rather than forcing a gc collection.
340
+ if sys.platform == "win32":
341
+ gc.collect()
342
+ gitdb.util.mman.collect()
343
+ if sys.platform == "win32":
344
+ gc.collect()
345
+
346
+ def __eq__(self, rhs: object) -> bool:
347
+ if isinstance(rhs, Repo):
348
+ return self.git_dir == rhs.git_dir
349
+ return False
350
+
351
+ def __ne__(self, rhs: object) -> bool:
352
+ return not self.__eq__(rhs)
353
+
354
+ def __hash__(self) -> int:
355
+ return hash(self.git_dir)
356
+
357
+ @property
358
+ def description(self) -> str:
359
+ """The project's description"""
360
+ filename = osp.join(self.git_dir, "description")
361
+ with open(filename, "rb") as fp:
362
+ return fp.read().rstrip().decode(defenc)
363
+
364
+ @description.setter
365
+ def description(self, descr: str) -> None:
366
+ filename = osp.join(self.git_dir, "description")
367
+ with open(filename, "wb") as fp:
368
+ fp.write((descr + "\n").encode(defenc))
369
+
370
+ @property
371
+ def working_tree_dir(self) -> Optional[PathLike]:
372
+ """
373
+ :return:
374
+ The working tree directory of our git repository.
375
+ If this is a bare repository, ``None`` is returned.
376
+ """
377
+ return self._working_tree_dir
378
+
379
+ @property
380
+ def common_dir(self) -> PathLike:
381
+ """
382
+ :return:
383
+ The git dir that holds everything except possibly HEAD, FETCH_HEAD,
384
+ ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
385
+ """
386
+ return self._common_dir or self.git_dir
387
+
388
+ @property
389
+ def bare(self) -> bool:
390
+ """:return: ``True`` if the repository is bare"""
391
+ return self._bare
392
+
393
+ @property
394
+ def heads(self) -> "IterableList[Head]":
395
+ """A list of :class:`~git.refs.head.Head` objects representing the branch heads
396
+ in this repo.
397
+
398
+ :return:
399
+ ``git.IterableList(Head, ...)``
400
+ """
401
+ return Head.list_items(self)
402
+
403
+ @property
404
+ def branches(self) -> "IterableList[Head]":
405
+ """Alias for heads.
406
+ A list of :class:`~git.refs.head.Head` objects representing the branch heads
407
+ in this repo.
408
+
409
+ :return:
410
+ ``git.IterableList(Head, ...)``
411
+ """
412
+ return self.heads
413
+
414
+ @property
415
+ def references(self) -> "IterableList[Reference]":
416
+ """A list of :class:`~git.refs.reference.Reference` objects representing tags,
417
+ heads and remote references.
418
+
419
+ :return:
420
+ ``git.IterableList(Reference, ...)``
421
+ """
422
+ return Reference.list_items(self)
423
+
424
+ @property
425
+ def refs(self) -> "IterableList[Reference]":
426
+ """Alias for references.
427
+ A list of :class:`~git.refs.reference.Reference` objects representing tags,
428
+ heads and remote references.
429
+
430
+ :return:
431
+ ``git.IterableList(Reference, ...)``
432
+ """
433
+ return self.references
434
+
435
+ @property
436
+ def index(self) -> "IndexFile":
437
+ """
438
+ :return:
439
+ A :class:`~git.index.base.IndexFile` representing this repository's index.
440
+
441
+ :note:
442
+ This property can be expensive, as the returned
443
+ :class:`~git.index.base.IndexFile` will be reinitialized.
444
+ It is recommended to reuse the object.
445
+ """
446
+ return IndexFile(self)
447
+
448
+ @property
449
+ def head(self) -> "HEAD":
450
+ """
451
+ :return:
452
+ :class:`~git.refs.head.HEAD` object pointing to the current head reference
453
+ """
454
+ return HEAD(self, "HEAD")
455
+
456
+ @property
457
+ def remotes(self) -> "IterableList[Remote]":
458
+ """A list of :class:`~git.remote.Remote` objects allowing to access and
459
+ manipulate remotes.
460
+
461
+ :return:
462
+ ``git.IterableList(Remote, ...)``
463
+ """
464
+ return Remote.list_items(self)
465
+
466
+ def remote(self, name: str = "origin") -> "Remote":
467
+ """:return: The remote with the specified name
468
+
469
+ :raise ValueError:
470
+ If no remote with such a name exists.
471
+ """
472
+ r = Remote(self, name)
473
+ if not r.exists():
474
+ raise ValueError("Remote named '%s' didn't exist" % name)
475
+ return r
476
+
477
+ # { Submodules
478
+
479
+ @property
480
+ def submodules(self) -> "IterableList[Submodule]":
481
+ """
482
+ :return:
483
+ git.IterableList(Submodule, ...) of direct submodules available from the
484
+ current head
485
+ """
486
+ return Submodule.list_items(self)
487
+
488
+ def submodule(self, name: str) -> "Submodule":
489
+ """:return: The submodule with the given name
490
+
491
+ :raise ValueError:
492
+ If no such submodule exists.
493
+ """
494
+ try:
495
+ return self.submodules[name]
496
+ except IndexError as e:
497
+ raise ValueError("Didn't find submodule named %r" % name) from e
498
+ # END exception handling
499
+
500
+ def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
501
+ """Create a new submodule.
502
+
503
+ :note:
504
+ For a description of the applicable parameters, see the documentation of
505
+ :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`.
506
+
507
+ :return:
508
+ The created submodule.
509
+ """
510
+ return Submodule.add(self, *args, **kwargs)
511
+
512
+ def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
513
+ """An iterator yielding Submodule instances.
514
+
515
+ See the :class:`~git.objects.util.Traversable` interface for a description of `args`
516
+ and `kwargs`.
517
+
518
+ :return:
519
+ Iterator
520
+ """
521
+ return RootModule(self).traverse(*args, **kwargs)
522
+
523
+ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
524
+ """Update the submodules, keeping the repository consistent as it will
525
+ take the previous state into consideration.
526
+
527
+ :note:
528
+ For more information, please see the documentation of
529
+ :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
530
+ """
531
+ return RootModule(self).update(*args, **kwargs)
532
+
533
+ # }END submodules
534
+
535
+ @property
536
+ def tags(self) -> "IterableList[TagReference]":
537
+ """A list of :class:`~git.refs.tag.TagReference` objects that are available in
538
+ this repo.
539
+
540
+ :return:
541
+ ``git.IterableList(TagReference, ...)``
542
+ """
543
+ return TagReference.list_items(self)
544
+
545
+ def tag(self, path: PathLike) -> TagReference:
546
+ """
547
+ :return:
548
+ :class:`~git.refs.tag.TagReference` object, reference pointing to a
549
+ :class:`~git.objects.commit.Commit` or tag
550
+
551
+ :param path:
552
+ Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``.
553
+ """
554
+ full_path = self._to_full_tag_path(path)
555
+ return TagReference(self, full_path)
556
+
557
+ @staticmethod
558
+ def _to_full_tag_path(path: PathLike) -> str:
559
+ path_str = str(path)
560
+ if path_str.startswith(TagReference._common_path_default + "/"):
561
+ return path_str
562
+ if path_str.startswith(TagReference._common_default + "/"):
563
+ return Reference._common_path_default + "/" + path_str
564
+ else:
565
+ return TagReference._common_path_default + "/" + path_str
566
+
567
+ def create_head(
568
+ self,
569
+ path: PathLike,
570
+ commit: Union["SymbolicReference", "str"] = "HEAD",
571
+ force: bool = False,
572
+ logmsg: Optional[str] = None,
573
+ ) -> "Head":
574
+ """Create a new head within the repository.
575
+
576
+ :note:
577
+ For more documentation, please see the
578
+ :meth:`Head.create <git.refs.head.Head.create>` method.
579
+
580
+ :return:
581
+ Newly created :class:`~git.refs.head.Head` Reference.
582
+ """
583
+ return Head.create(self, path, commit, logmsg, force)
584
+
585
+ def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
586
+ """Delete the given heads.
587
+
588
+ :param kwargs:
589
+ Additional keyword arguments to be passed to :manpage:`git-branch(1)`.
590
+ """
591
+ return Head.delete(self, *heads, **kwargs)
592
+
593
+ def create_tag(
594
+ self,
595
+ path: PathLike,
596
+ ref: Union[str, "SymbolicReference"] = "HEAD",
597
+ message: Optional[str] = None,
598
+ force: bool = False,
599
+ **kwargs: Any,
600
+ ) -> TagReference:
601
+ """Create a new tag reference.
602
+
603
+ :note:
604
+ For more documentation, please see the
605
+ :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
606
+
607
+ :return:
608
+ :class:`~git.refs.tag.TagReference` object
609
+ """
610
+ return TagReference.create(self, path, ref, message, force, **kwargs)
611
+
612
+ def delete_tag(self, *tags: TagReference) -> None:
613
+ """Delete the given tag references."""
614
+ return TagReference.delete(self, *tags)
615
+
616
+ def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
617
+ """Create a new remote.
618
+
619
+ For more information, please see the documentation of the
620
+ :meth:`Remote.create <git.remote.Remote.create>` method.
621
+
622
+ :return:
623
+ :class:`~git.remote.Remote` reference
624
+ """
625
+ return Remote.create(self, name, url, **kwargs)
626
+
627
+ def delete_remote(self, remote: "Remote") -> str:
628
+ """Delete the given remote."""
629
+ return Remote.remove(self, remote)
630
+
631
+ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str:
632
+ if git_dir is None:
633
+ git_dir = self.git_dir
634
+ # We do not support an absolute path of the gitconfig on Windows.
635
+ # Use the global config instead.
636
+ if sys.platform == "win32" and config_level == "system":
637
+ config_level = "global"
638
+
639
+ if config_level == "system":
640
+ return "/etc/gitconfig"
641
+ elif config_level == "user":
642
+ config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
643
+ return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
644
+ elif config_level == "global":
645
+ return osp.normpath(osp.expanduser("~/.gitconfig"))
646
+ elif config_level == "repository":
647
+ repo_dir = self._common_dir or git_dir
648
+ if not repo_dir:
649
+ raise NotADirectoryError
650
+ else:
651
+ return osp.normpath(osp.join(repo_dir, "config"))
652
+ else:
653
+ assert_never( # type: ignore[unreachable]
654
+ config_level,
655
+ ValueError(f"Invalid configuration level: {config_level!r}"),
656
+ )
657
+
658
+ def config_reader(
659
+ self,
660
+ config_level: Optional[Lit_config_levels] = None,
661
+ ) -> GitConfigParser:
662
+ """
663
+ :return:
664
+ :class:`~git.config.GitConfigParser` allowing to read the full git
665
+ configuration, but not to write it.
666
+
667
+ The configuration will include values from the system, user and repository
668
+ configuration files.
669
+
670
+ :param config_level:
671
+ For possible values, see the :meth:`config_writer` method. If ``None``, all
672
+ applicable levels will be used. Specify a level in case you know which file
673
+ you wish to read to prevent reading multiple files.
674
+
675
+ :note:
676
+ On Windows, system configuration cannot currently be read as the path is
677
+ unknown, instead the global path will be used.
678
+ """
679
+ return self._config_reader(config_level=config_level)
680
+
681
+ def _config_reader(
682
+ self,
683
+ config_level: Optional[Lit_config_levels] = None,
684
+ git_dir: Optional[PathLike] = None,
685
+ ) -> GitConfigParser:
686
+ if config_level is None:
687
+ files = [
688
+ self._get_config_path(cast(Lit_config_levels, f), git_dir)
689
+ for f in self.config_level
690
+ if cast(Lit_config_levels, f)
691
+ ]
692
+ else:
693
+ files = [self._get_config_path(config_level, git_dir)]
694
+ return GitConfigParser(files, read_only=True, repo=self)
695
+
696
+ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
697
+ """
698
+ :return:
699
+ A :class:`~git.config.GitConfigParser` allowing to write values of the
700
+ specified configuration file level. Config writers should be retrieved, used
701
+ to change the configuration, and written right away as they will lock the
702
+ configuration file in question and prevent other's to write it.
703
+
704
+ :param config_level:
705
+ One of the following values:
706
+
707
+ * ``"system"`` = system wide configuration file
708
+ * ``"global"`` = user level configuration file
709
+ * ``"`repository"`` = configuration file for this repository only
710
+ """
711
+ return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False)
712
+
713
+ def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
714
+ """The :class:`~git.objects.commit.Commit` object for the specified revision.
715
+
716
+ :param rev:
717
+ Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
718
+
719
+ :return:
720
+ :class:`~git.objects.commit.Commit`
721
+ """
722
+ if rev is None:
723
+ return self.head.commit
724
+ return self.rev_parse(str(rev) + "^0")
725
+
726
+ def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
727
+ """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects
728
+
729
+ :note:
730
+ Accepts all arguments known to the :meth:`iter_commits` method.
731
+ """
732
+ return (c.tree for c in self.iter_commits(*args, **kwargs))
733
+
734
+ def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
735
+ """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision.
736
+
737
+ Examples::
738
+
739
+ repo.tree(repo.heads[0])
740
+
741
+ :param rev:
742
+ A revision pointing to a Treeish (being a commit or tree).
743
+
744
+ :return:
745
+ :class:`~git.objects.tree.Tree`
746
+
747
+ :note:
748
+ If you need a non-root level tree, find it by iterating the root tree.
749
+ Otherwise it cannot know about its path relative to the repository root and
750
+ subsequent operations might have unexpected results.
751
+ """
752
+ if rev is None:
753
+ return self.head.commit.tree
754
+ return self.rev_parse(str(rev) + "^{tree}")
755
+
756
+ def iter_commits(
757
+ self,
758
+ rev: Union[str, Commit, "SymbolicReference", None] = None,
759
+ paths: Union[PathLike, Sequence[PathLike]] = "",
760
+ **kwargs: Any,
761
+ ) -> Iterator[Commit]:
762
+ """An iterator of :class:`~git.objects.commit.Commit` objects representing the
763
+ history of a given ref/commit.
764
+
765
+ :param rev:
766
+ Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
767
+ If ``None``, the active branch will be used.
768
+
769
+ :param paths:
770
+ An optional path or a list of paths. If set, only commits that include the
771
+ path or paths will be returned.
772
+
773
+ :param kwargs:
774
+ Arguments to be passed to :manpage:`git-rev-list(1)`.
775
+ Common ones are ``max_count`` and ``skip``.
776
+
777
+ :note:
778
+ To receive only commits between two named revisions, use the
779
+ ``"revA...revB"`` revision specifier.
780
+
781
+ :return:
782
+ Iterator of :class:`~git.objects.commit.Commit` objects
783
+ """
784
+ if rev is None:
785
+ rev = self.head.commit
786
+
787
+ return Commit.iter_items(self, rev, paths, **kwargs)
788
+
789
+ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
790
+ R"""Find the closest common ancestor for the given revision
791
+ (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
792
+ :class:`~git.refs.reference.Reference`\s, etc.).
793
+
794
+ :param rev:
795
+ At least two revs to find the common ancestor for.
796
+
797
+ :param kwargs:
798
+ Additional arguments to be passed to the ``repo.git.merge_base()`` command
799
+ which does all the work.
800
+
801
+ :return:
802
+ A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
803
+ not passed as a keyword argument, the list will have at max one
804
+ :class:`~git.objects.commit.Commit`, or is empty if no common merge base
805
+ exists.
806
+
807
+ :raise ValueError:
808
+ If fewer than two revisions are provided.
809
+ """
810
+ if len(rev) < 2:
811
+ raise ValueError("Please specify at least two revs, got only %i" % len(rev))
812
+ # END handle input
813
+
814
+ res: List[Commit] = []
815
+ try:
816
+ lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
817
+ except GitCommandError as err:
818
+ if err.status == 128:
819
+ raise
820
+ # END handle invalid rev
821
+ # Status code 1 is returned if there is no merge-base.
822
+ # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19)
823
+ return res
824
+ # END exception handling
825
+
826
+ for line in lines:
827
+ res.append(self.commit(line))
828
+ # END for each merge-base
829
+
830
+ return res
831
+
832
+ def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
833
+ """Check if a commit is an ancestor of another.
834
+
835
+ :param ancestor_rev:
836
+ Rev which should be an ancestor.
837
+
838
+ :param rev:
839
+ Rev to test against `ancestor_rev`.
840
+
841
+ :return:
842
+ ``True`` if `ancestor_rev` is an ancestor to `rev`.
843
+ """
844
+ try:
845
+ self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
846
+ except GitCommandError as err:
847
+ if err.status == 1:
848
+ return False
849
+ raise
850
+ return True
851
+
852
+ def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> bool:
853
+ try:
854
+ complete_sha = self.odb.partial_to_complete_sha_hex(sha)
855
+ object_info = self.odb.info(complete_sha)
856
+ if object_type:
857
+ if object_info.type == object_type.encode():
858
+ return True
859
+ else:
860
+ _logger.debug(
861
+ "Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
862
+ object_info.type.decode(),
863
+ object_type,
864
+ )
865
+ return False
866
+ else:
867
+ return True
868
+ except BadObject:
869
+ _logger.debug("Commit hash is invalid.")
870
+ return False
871
+
872
+ def _get_daemon_export(self) -> bool:
873
+ if self.git_dir:
874
+ filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
875
+ return osp.exists(filename)
876
+
877
+ def _set_daemon_export(self, value: object) -> None:
878
+ if self.git_dir:
879
+ filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
880
+ fileexists = osp.exists(filename)
881
+ if value and not fileexists:
882
+ touch(filename)
883
+ elif not value and fileexists:
884
+ os.unlink(filename)
885
+
886
+ @property
887
+ def daemon_export(self) -> bool:
888
+ """If True, git-daemon may export this repository"""
889
+ return self._get_daemon_export()
890
+
891
+ @daemon_export.setter
892
+ def daemon_export(self, value: object) -> None:
893
+ self._set_daemon_export(value)
894
+
895
+ def _get_alternates(self) -> List[str]:
896
+ """The list of alternates for this repo from which objects can be retrieved.
897
+
898
+ :return:
899
+ List of strings being pathnames of alternates
900
+ """
901
+ if self.git_dir:
902
+ alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
903
+
904
+ if osp.exists(alternates_path):
905
+ with open(alternates_path, "rb") as f:
906
+ alts = f.read().decode(defenc)
907
+ return alts.strip().splitlines()
908
+ return []
909
+
910
+ def _set_alternates(self, alts: List[str]) -> None:
911
+ """Set the alternates.
912
+
913
+ :param alts:
914
+ The array of string paths representing the alternates at which git should
915
+ look for objects, i.e. ``/home/user/repo/.git/objects``.
916
+
917
+ :raise git.exc.NoSuchPathError:
918
+
919
+ :note:
920
+ The method does not check for the existence of the paths in `alts`, as the
921
+ caller is responsible.
922
+ """
923
+ alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
924
+ if not alts:
925
+ if osp.isfile(alternates_path):
926
+ os.remove(alternates_path)
927
+ else:
928
+ with open(alternates_path, "wb") as f:
929
+ f.write("\n".join(alts).encode(defenc))
930
+
931
+ @property
932
+ def alternates(self) -> List[str]:
933
+ """Retrieve a list of alternates paths or set a list paths to be used as alternates"""
934
+ return self._get_alternates()
935
+
936
+ @alternates.setter
937
+ def alternates(self, alts: List[str]) -> None:
938
+ self._set_alternates(alts)
939
+
940
+ def is_dirty(
941
+ self,
942
+ index: bool = True,
943
+ working_tree: bool = True,
944
+ untracked_files: bool = False,
945
+ submodules: bool = True,
946
+ path: Optional[PathLike] = None,
947
+ ) -> bool:
948
+ """
949
+ :return:
950
+ ``True`` if the repository is considered dirty. By default it will react
951
+ like a :manpage:`git-status(1)` without untracked files, hence it is dirty
952
+ if the index or the working copy have changes.
953
+ """
954
+ if self._bare:
955
+ # Bare repositories with no associated working directory are
956
+ # always considered to be clean.
957
+ return False
958
+
959
+ # Start from the one which is fastest to evaluate.
960
+ default_args = ["--abbrev=40", "--full-index", "--raw"]
961
+ if not submodules:
962
+ default_args.append("--ignore-submodules")
963
+ if path:
964
+ default_args.extend(["--", str(path)])
965
+ if index:
966
+ # diff index against HEAD.
967
+ if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
968
+ return True
969
+ # END index handling
970
+ if working_tree:
971
+ # diff index against working tree.
972
+ if len(self.git.diff(*default_args)):
973
+ return True
974
+ # END working tree handling
975
+ if untracked_files:
976
+ if len(self._get_untracked_files(path, ignore_submodules=not submodules)):
977
+ return True
978
+ # END untracked files
979
+ return False
980
+
981
+ @property
982
+ def untracked_files(self) -> List[str]:
983
+ """
984
+ :return:
985
+ list(str,...)
986
+
987
+ Files currently untracked as they have not been staged yet. Paths are
988
+ relative to the current working directory of the git command.
989
+
990
+ :note:
991
+ Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
992
+
993
+ :note:
994
+ This property is expensive, as no cache is involved. To process the result,
995
+ please consider caching it yourself.
996
+ """
997
+ return self._get_untracked_files()
998
+
999
+ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
1000
+ # Make sure we get all files, not only untracked directories.
1001
+ proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs)
1002
+ # Untracked files prefix in porcelain mode
1003
+ prefix = "?? "
1004
+ untracked_files = []
1005
+ for line in proc.stdout:
1006
+ line = line.decode(defenc)
1007
+ if not line.startswith(prefix):
1008
+ continue
1009
+ filename = line[len(prefix) :].rstrip("\n")
1010
+ # Special characters are escaped
1011
+ if filename[0] == filename[-1] == '"':
1012
+ filename = filename[1:-1]
1013
+ # WHATEVER ... it's a mess, but works for me
1014
+ filename = filename.encode("ascii").decode("unicode_escape").encode("latin1").decode(defenc)
1015
+ untracked_files.append(filename)
1016
+ finalize_process(proc)
1017
+ return untracked_files
1018
+
1019
+ def ignored(self, *paths: PathLike) -> List[str]:
1020
+ """Checks if paths are ignored via ``.gitignore``.
1021
+
1022
+ This does so using the :manpage:`git-check-ignore(1)` method.
1023
+
1024
+ :param paths:
1025
+ List of paths to check whether they are ignored or not.
1026
+
1027
+ :return:
1028
+ Subset of those paths which are ignored
1029
+ """
1030
+ try:
1031
+ proc: str = self.git.check_ignore(*paths)
1032
+ except GitCommandError as err:
1033
+ if err.status == 1:
1034
+ # If return code is 1, this means none of the items in *paths are
1035
+ # ignored by Git, so return an empty list.
1036
+ return []
1037
+ else:
1038
+ # Raise the exception on all other return codes.
1039
+ raise
1040
+
1041
+ return proc.replace("\\\\", "\\").replace('"', "").split("\n")
1042
+
1043
+ @property
1044
+ def active_branch(self) -> Head:
1045
+ """The name of the currently active branch.
1046
+
1047
+ :raise TypeError:
1048
+ If HEAD is detached.
1049
+
1050
+ :return:
1051
+ :class:`~git.refs.head.Head` to the active branch
1052
+ """
1053
+ # reveal_type(self.head.reference) # => Reference
1054
+ return self.head.reference
1055
+
1056
+ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
1057
+ """Iterator for blame information for the given file at the given revision.
1058
+
1059
+ Unlike :meth:`blame`, this does not return the actual file's contents, only a
1060
+ stream of :class:`BlameEntry` tuples.
1061
+
1062
+ :param rev:
1063
+ Revision specifier. If ``None``, the blame will include all the latest
1064
+ uncommitted changes. Otherwise, anything successfully parsed by
1065
+ :manpage:`git-rev-parse(1)` is a valid option.
1066
+
1067
+ :return:
1068
+ Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
1069
+ commit to blame for the line, and range indicates a span of line numbers in
1070
+ the resulting file.
1071
+
1072
+ If you combine all line number ranges outputted by this command, you should get
1073
+ a continuous range spanning all line numbers in the file.
1074
+ """
1075
+
1076
+ data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
1077
+ commits: Dict[bytes, Commit] = {}
1078
+
1079
+ stream = (line for line in data.split(b"\n") if line)
1080
+ while True:
1081
+ try:
1082
+ # When exhausted, causes a StopIteration, terminating this function.
1083
+ line = next(stream)
1084
+ except StopIteration:
1085
+ return
1086
+ split_line = line.split()
1087
+ hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line
1088
+ lineno = int(lineno_b)
1089
+ num_lines = int(num_lines_b)
1090
+ orig_lineno = int(orig_lineno_b)
1091
+ if hexsha not in commits:
1092
+ # Now read the next few lines and build up a dict of properties for this
1093
+ # commit.
1094
+ props: Dict[bytes, bytes] = {}
1095
+ while True:
1096
+ try:
1097
+ line = next(stream)
1098
+ except StopIteration:
1099
+ return
1100
+ if line == b"boundary":
1101
+ # "boundary" indicates a root commit and occurs instead of the
1102
+ # "previous" tag.
1103
+ continue
1104
+
1105
+ tag, value = line.split(b" ", 1)
1106
+ props[tag] = value
1107
+ if tag == b"filename":
1108
+ # "filename" formally terminates the entry for --incremental.
1109
+ orig_filename = value
1110
+ break
1111
+
1112
+ c = Commit(
1113
+ self,
1114
+ hex_to_bin(hexsha),
1115
+ author=Actor(
1116
+ safe_decode(props[b"author"]),
1117
+ safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")),
1118
+ ),
1119
+ authored_date=int(props[b"author-time"]),
1120
+ committer=Actor(
1121
+ safe_decode(props[b"committer"]),
1122
+ safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")),
1123
+ ),
1124
+ committed_date=int(props[b"committer-time"]),
1125
+ )
1126
+ commits[hexsha] = c
1127
+ else:
1128
+ # Discard all lines until we find "filename" which is guaranteed to be
1129
+ # the last line.
1130
+ while True:
1131
+ try:
1132
+ # Will fail if we reach the EOF unexpectedly.
1133
+ line = next(stream)
1134
+ except StopIteration:
1135
+ return
1136
+ tag, value = line.split(b" ", 1)
1137
+ if tag == b"filename":
1138
+ orig_filename = value
1139
+ break
1140
+
1141
+ yield BlameEntry(
1142
+ commits[hexsha],
1143
+ range(lineno, lineno + num_lines),
1144
+ safe_decode(orig_filename),
1145
+ range(orig_lineno, orig_lineno + num_lines),
1146
+ )
1147
+
1148
+ def blame(
1149
+ self,
1150
+ rev: Union[str, HEAD, None],
1151
+ file: str,
1152
+ incremental: bool = False,
1153
+ rev_opts: Optional[List[str]] = None,
1154
+ **kwargs: Any,
1155
+ ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
1156
+ """The blame information for the given file at the given revision.
1157
+
1158
+ :param rev:
1159
+ Revision specifier. If ``None``, the blame will include all the latest
1160
+ uncommitted changes. Otherwise, anything successfully parsed by
1161
+ :manpage:`git-rev-parse(1)` is a valid option.
1162
+
1163
+ :return:
1164
+ list: [git.Commit, list: [<line>]]
1165
+
1166
+ A list of lists associating a :class:`~git.objects.commit.Commit` object
1167
+ with a list of lines that changed within the given commit. The
1168
+ :class:`~git.objects.commit.Commit` objects will be given in order of
1169
+ appearance.
1170
+ """
1171
+ if incremental:
1172
+ return self.blame_incremental(rev, file, **kwargs)
1173
+ rev_opts = rev_opts or []
1174
+ data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)
1175
+ commits: Dict[str, Commit] = {}
1176
+ blames: List[List[Commit | List[str | bytes] | None]] = []
1177
+
1178
+ class InfoTD(TypedDict, total=False):
1179
+ sha: str
1180
+ id: str
1181
+ filename: str
1182
+ summary: str
1183
+ author: str
1184
+ author_email: str
1185
+ author_date: int
1186
+ committer: str
1187
+ committer_email: str
1188
+ committer_date: int
1189
+
1190
+ info: InfoTD = {}
1191
+
1192
+ keepends = True
1193
+ for line_bytes in data.splitlines(keepends):
1194
+ try:
1195
+ line_str = line_bytes.rstrip().decode(defenc)
1196
+ except UnicodeDecodeError:
1197
+ firstpart = ""
1198
+ parts = []
1199
+ is_binary = True
1200
+ else:
1201
+ # As we don't have an idea when the binary data ends, as it could
1202
+ # contain multiple newlines in the process. So we rely on being able to
1203
+ # decode to tell us what it is. This can absolutely fail even on text
1204
+ # files, but even if it does, we should be fine treating it as binary
1205
+ # instead.
1206
+ parts = self.re_whitespace.split(line_str, 1)
1207
+ firstpart = parts[0]
1208
+ is_binary = False
1209
+ # END handle decode of line
1210
+
1211
+ if self.re_hexsha_only.search(firstpart):
1212
+ # handles
1213
+ # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start
1214
+ # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates
1215
+ # another line of blame with the same data
1216
+ digits = parts[-1].split(" ")
1217
+ if len(digits) == 3:
1218
+ info = {"id": firstpart}
1219
+ blames.append([None, []])
1220
+ elif info["id"] != firstpart:
1221
+ info = {"id": firstpart}
1222
+ blames.append([commits.get(firstpart), []])
1223
+ # END blame data initialization
1224
+ else:
1225
+ m = self.re_author_committer_start.search(firstpart)
1226
+ if m:
1227
+ # handles:
1228
+ # author Tom Preston-Werner
1229
+ # author-mail <tom@mojombo.com>
1230
+ # author-time 1192271832
1231
+ # author-tz -0700
1232
+ # committer Tom Preston-Werner
1233
+ # committer-mail <tom@mojombo.com>
1234
+ # committer-time 1192271832
1235
+ # committer-tz -0700 - IGNORED BY US
1236
+ role = m.group(0)
1237
+ if role == "author":
1238
+ if firstpart.endswith("-mail"):
1239
+ info["author_email"] = parts[-1]
1240
+ elif firstpart.endswith("-time"):
1241
+ info["author_date"] = int(parts[-1])
1242
+ elif role == firstpart:
1243
+ info["author"] = parts[-1]
1244
+ elif role == "committer":
1245
+ if firstpart.endswith("-mail"):
1246
+ info["committer_email"] = parts[-1]
1247
+ elif firstpart.endswith("-time"):
1248
+ info["committer_date"] = int(parts[-1])
1249
+ elif role == firstpart:
1250
+ info["committer"] = parts[-1]
1251
+ # END distinguish mail,time,name
1252
+ else:
1253
+ # handle
1254
+ # filename lib/grit.rb
1255
+ # summary add Blob
1256
+ # <and rest>
1257
+ if firstpart.startswith("filename"):
1258
+ info["filename"] = parts[-1]
1259
+ elif firstpart.startswith("summary"):
1260
+ info["summary"] = parts[-1]
1261
+ elif firstpart == "":
1262
+ if info:
1263
+ sha = info["id"]
1264
+ c = commits.get(sha)
1265
+ if c is None:
1266
+ c = Commit(
1267
+ self,
1268
+ hex_to_bin(sha),
1269
+ author=Actor._from_string(f"{info['author']} {info['author_email']}"),
1270
+ authored_date=info["author_date"],
1271
+ committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"),
1272
+ committed_date=info["committer_date"],
1273
+ )
1274
+ commits[sha] = c
1275
+ blames[-1][0] = c
1276
+ # END if commit objects needs initial creation
1277
+
1278
+ if blames[-1][1] is not None:
1279
+ line: str | bytes
1280
+ if not is_binary:
1281
+ if line_str and line_str[0] == "\t":
1282
+ line_str = line_str[1:]
1283
+ line = line_str
1284
+ else:
1285
+ line = line_bytes
1286
+ # NOTE: We are actually parsing lines out of binary
1287
+ # data, which can lead to the binary being split up
1288
+ # along the newline separator. We will append this
1289
+ # to the blame we are currently looking at, even
1290
+ # though it should be concatenated with the last
1291
+ # line we have seen.
1292
+ blames[-1][1].append(line)
1293
+
1294
+ info = {"id": sha}
1295
+ # END if we collected commit info
1296
+ # END distinguish filename,summary,rest
1297
+ # END distinguish author|committer vs filename,summary,rest
1298
+ # END distinguish hexsha vs other information
1299
+ return blames
1300
+
1301
+ @classmethod
1302
+ def init(
1303
+ cls,
1304
+ path: Union[PathLike, None] = None,
1305
+ mkdir: bool = True,
1306
+ odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1307
+ expand_vars: bool = True,
1308
+ **kwargs: Any,
1309
+ ) -> "Repo":
1310
+ """Initialize a git repository at the given path if specified.
1311
+
1312
+ :param path:
1313
+ The full path to the repo (traditionally ends with ``/<name>.git``). Or
1314
+ ``None``, in which case the repository will be created in the current
1315
+ working directory.
1316
+
1317
+ :param mkdir:
1318
+ If specified, will create the repository directory if it doesn't already
1319
+ exist. Creates the directory with a mode=0755.
1320
+ Only effective if a path is explicitly given.
1321
+
1322
+ :param odbt:
1323
+ Object DataBase type - a type which is constructed by providing the
1324
+ directory containing the database objects, i.e. ``.git/objects``. It will be
1325
+ used to access all object data.
1326
+
1327
+ :param expand_vars:
1328
+ If specified, environment variables will not be escaped. This can lead to
1329
+ information disclosure, allowing attackers to access the contents of
1330
+ environment variables.
1331
+
1332
+ :param kwargs:
1333
+ Keyword arguments serving as additional options to the
1334
+ :manpage:`git-init(1)` command.
1335
+
1336
+ :return:
1337
+ :class:`Repo` (the newly created repo)
1338
+ """
1339
+ if path:
1340
+ path = expand_path(path, expand_vars)
1341
+ if mkdir and path and not osp.exists(path):
1342
+ os.makedirs(path, 0o755)
1343
+
1344
+ # git command automatically chdir into the directory
1345
+ git = cls.GitCommandWrapperType(path)
1346
+ git.init(**kwargs)
1347
+ return cls(path, odbt=odbt)
1348
+
1349
+ @classmethod
1350
+ def _clone(
1351
+ cls,
1352
+ git: "Git",
1353
+ url: PathLike,
1354
+ path: PathLike,
1355
+ odb_default_type: Type[GitCmdObjectDB],
1356
+ progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None,
1357
+ multi_options: Optional[List[str]] = None,
1358
+ allow_unsafe_protocols: bool = False,
1359
+ allow_unsafe_options: bool = False,
1360
+ **kwargs: Any,
1361
+ ) -> "Repo":
1362
+ odbt = kwargs.pop("odbt", odb_default_type)
1363
+
1364
+ # When pathlib.Path or other class-based path is passed
1365
+ if not isinstance(path, str):
1366
+ path = str(path)
1367
+
1368
+ ## A bug win cygwin's Git, when `--bare` or `--separate-git-dir`
1369
+ # it prepends the cwd or(?) the `url` into the `path, so::
1370
+ # git clone --bare /cygwin/d/foo.git C:\\Work
1371
+ # becomes::
1372
+ # git clone --bare /cygwin/d/foo.git /cygwin/d/C:\\Work
1373
+ #
1374
+ clone_path = Git.polish_url(path) if Git.is_cygwin() and "bare" in kwargs else path
1375
+ sep_dir = kwargs.get("separate_git_dir")
1376
+ if sep_dir:
1377
+ kwargs["separate_git_dir"] = Git.polish_url(sep_dir)
1378
+ multi = None
1379
+ if multi_options:
1380
+ multi = shlex.split(" ".join(multi_options))
1381
+
1382
+ if not allow_unsafe_protocols:
1383
+ Git.check_unsafe_protocols(str(url))
1384
+ if not allow_unsafe_options:
1385
+ Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
1386
+ if not allow_unsafe_options and multi_options:
1387
+ Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)
1388
+
1389
+ proc = git.clone(
1390
+ multi,
1391
+ "--",
1392
+ Git.polish_url(str(url)),
1393
+ clone_path,
1394
+ with_extended_output=True,
1395
+ as_process=True,
1396
+ v=True,
1397
+ universal_newlines=True,
1398
+ **add_progress(kwargs, git, progress),
1399
+ )
1400
+ if progress:
1401
+ handle_process_output(
1402
+ proc,
1403
+ None,
1404
+ to_progress_instance(progress).new_message_handler(),
1405
+ finalize_process,
1406
+ decode_streams=False,
1407
+ )
1408
+ else:
1409
+ (stdout, stderr) = proc.communicate()
1410
+ cmdline = getattr(proc, "args", "")
1411
+ cmdline = remove_password_if_present(cmdline)
1412
+
1413
+ _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
1414
+ finalize_process(proc, stderr=stderr)
1415
+
1416
+ # Our git command could have a different working dir than our actual
1417
+ # environment, hence we prepend its working dir if required.
1418
+ if not osp.isabs(path):
1419
+ path = osp.join(git._working_dir, path) if git._working_dir is not None else path
1420
+
1421
+ repo = cls(path, odbt=odbt)
1422
+
1423
+ # Retain env values that were passed to _clone().
1424
+ repo.git.update_environment(**git.environment())
1425
+
1426
+ # Adjust remotes - there may be operating systems which use backslashes, These
1427
+ # might be given as initial paths, but when handling the config file that
1428
+ # contains the remote from which we were clones, git stops liking it as it will
1429
+ # escape the backslashes. Hence we undo the escaping just to be sure.
1430
+ if repo.remotes:
1431
+ with repo.remotes[0].config_writer as writer:
1432
+ writer.set_value("url", Git.polish_url(repo.remotes[0].url))
1433
+ # END handle remote repo
1434
+ return repo
1435
+
1436
+ def clone(
1437
+ self,
1438
+ path: PathLike,
1439
+ progress: Optional[CallableProgress] = None,
1440
+ multi_options: Optional[List[str]] = None,
1441
+ allow_unsafe_protocols: bool = False,
1442
+ allow_unsafe_options: bool = False,
1443
+ **kwargs: Any,
1444
+ ) -> "Repo":
1445
+ """Create a clone from this repository.
1446
+
1447
+ :param path:
1448
+ The full path of the new repo (traditionally ends with ``./<name>.git``).
1449
+
1450
+ :param progress:
1451
+ See :meth:`Remote.push <git.remote.Remote.push>`.
1452
+
1453
+ :param multi_options:
1454
+ A list of :manpage:`git-clone(1)` options that can be provided multiple
1455
+ times.
1456
+
1457
+ One option per list item which is passed exactly as specified to clone.
1458
+ For example::
1459
+
1460
+ [
1461
+ "--config core.filemode=false",
1462
+ "--config core.ignorecase",
1463
+ "--recurse-submodule=repo1_path",
1464
+ "--recurse-submodule=repo2_path",
1465
+ ]
1466
+
1467
+ :param allow_unsafe_protocols:
1468
+ Allow unsafe protocols to be used, like ``ext``.
1469
+
1470
+ :param allow_unsafe_options:
1471
+ Allow unsafe options to be used, like ``--upload-pack``.
1472
+
1473
+ :param kwargs:
1474
+ * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
1475
+ implementation used by the returned :class:`Repo` instance.
1476
+ * All remaining keyword arguments are given to the :manpage:`git-clone(1)`
1477
+ command.
1478
+
1479
+ :return:
1480
+ :class:`Repo` (the newly cloned repo)
1481
+ """
1482
+ return self._clone(
1483
+ self.git,
1484
+ self.common_dir,
1485
+ path,
1486
+ type(self.odb),
1487
+ progress,
1488
+ multi_options,
1489
+ allow_unsafe_protocols=allow_unsafe_protocols,
1490
+ allow_unsafe_options=allow_unsafe_options,
1491
+ **kwargs,
1492
+ )
1493
+
1494
+ @classmethod
1495
+ def clone_from(
1496
+ cls,
1497
+ url: PathLike,
1498
+ to_path: PathLike,
1499
+ progress: CallableProgress = None,
1500
+ env: Optional[Mapping[str, str]] = None,
1501
+ multi_options: Optional[List[str]] = None,
1502
+ allow_unsafe_protocols: bool = False,
1503
+ allow_unsafe_options: bool = False,
1504
+ **kwargs: Any,
1505
+ ) -> "Repo":
1506
+ """Create a clone from the given URL.
1507
+
1508
+ :param url:
1509
+ Valid git url, see: https://git-scm.com/docs/git-clone#URLS
1510
+
1511
+ :param to_path:
1512
+ Path to which the repository should be cloned to.
1513
+
1514
+ :param progress:
1515
+ See :meth:`Remote.push <git.remote.Remote.push>`.
1516
+
1517
+ :param env:
1518
+ Optional dictionary containing the desired environment variables.
1519
+
1520
+ Note: Provided variables will be used to update the execution environment
1521
+ for ``git``. If some variable is not specified in `env` and is defined in
1522
+ :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want
1523
+ to unset some variable, consider providing empty string as its value.
1524
+
1525
+ :param multi_options:
1526
+ See the :meth:`clone` method.
1527
+
1528
+ :param allow_unsafe_protocols:
1529
+ Allow unsafe protocols to be used, like ``ext``.
1530
+
1531
+ :param allow_unsafe_options:
1532
+ Allow unsafe options to be used, like ``--upload-pack``.
1533
+
1534
+ :param kwargs:
1535
+ See the :meth:`clone` method.
1536
+
1537
+ :return:
1538
+ :class:`Repo` instance pointing to the cloned directory.
1539
+ """
1540
+ git = cls.GitCommandWrapperType(os.getcwd())
1541
+ if env is not None:
1542
+ git.update_environment(**env)
1543
+ return cls._clone(
1544
+ git,
1545
+ url,
1546
+ to_path,
1547
+ GitCmdObjectDB,
1548
+ progress,
1549
+ multi_options,
1550
+ allow_unsafe_protocols=allow_unsafe_protocols,
1551
+ allow_unsafe_options=allow_unsafe_options,
1552
+ **kwargs,
1553
+ )
1554
+
1555
+ def archive(
1556
+ self,
1557
+ ostream: Union[TextIO, BinaryIO],
1558
+ treeish: Optional[str] = None,
1559
+ prefix: Optional[str] = None,
1560
+ **kwargs: Any,
1561
+ ) -> Repo:
1562
+ """Archive the tree at the given revision.
1563
+
1564
+ :param ostream:
1565
+ File-compatible stream object to which the archive will be written as bytes.
1566
+
1567
+ :param treeish:
1568
+ The treeish name/id, defaults to active branch.
1569
+
1570
+ :param prefix:
1571
+ The optional prefix to prepend to each filename in the archive.
1572
+
1573
+ :param kwargs:
1574
+ Additional arguments passed to :manpage:`git-archive(1)`:
1575
+
1576
+ * Use the ``format`` argument to define the kind of format. Use specialized
1577
+ ostreams to write any format supported by Python.
1578
+ * You may specify the special ``path`` keyword, which may either be a
1579
+ repository-relative path to a directory or file to place into the archive,
1580
+ or a list or tuple of multiple paths.
1581
+
1582
+ :raise git.exc.GitCommandError:
1583
+ If something went wrong.
1584
+
1585
+ :return:
1586
+ self
1587
+ """
1588
+ if treeish is None:
1589
+ treeish = self.head.commit
1590
+ if prefix and "prefix" not in kwargs:
1591
+ kwargs["prefix"] = prefix
1592
+ kwargs["output_stream"] = ostream
1593
+ path = kwargs.pop("path", [])
1594
+ path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
1595
+ if not isinstance(path, (tuple, list)):
1596
+ path = [path]
1597
+ # END ensure paths is list (or tuple)
1598
+ self.git.archive("--", treeish, *path, **kwargs)
1599
+ return self
1600
+
1601
+ def has_separate_working_tree(self) -> bool:
1602
+ """
1603
+ :return:
1604
+ True if our :attr:`git_dir` is not at the root of our
1605
+ :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic
1606
+ symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points
1607
+ to.
1608
+
1609
+ :note:
1610
+ Bare repositories will always return ``False`` here.
1611
+ """
1612
+ if self.bare:
1613
+ return False
1614
+ if self.working_tree_dir:
1615
+ return osp.isfile(osp.join(self.working_tree_dir, ".git"))
1616
+ else:
1617
+ return False # Or raise Error?
1618
+
1619
+ rev_parse = rev_parse
1620
+
1621
+ def __repr__(self) -> str:
1622
+ clazz = self.__class__
1623
+ return "<%s.%s %r>" % (clazz.__module__, clazz.__name__, self.git_dir)
1624
+
1625
+ def currently_rebasing_on(self) -> Commit | None:
1626
+ """
1627
+ :return:
1628
+ The commit which is currently being replayed while rebasing.
1629
+
1630
+ ``None`` if we are not currently rebasing.
1631
+ """
1632
+ if self.git_dir:
1633
+ rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1634
+ if not osp.isfile(rebase_head_file):
1635
+ return None
1636
+ with open(rebase_head_file, "rt") as f:
1637
+ content = f.readline().strip()
1638
+ return self.commit(content)
.venv/lib/python3.11/site-packages/git/repo/fun.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """General repository-related functions."""
5
+
6
+ from __future__ import annotations
7
+
8
+ __all__ = [
9
+ "rev_parse",
10
+ "is_git_dir",
11
+ "touch",
12
+ "find_submodule_git_dir",
13
+ "name_to_object",
14
+ "short_to_long",
15
+ "deref_tag",
16
+ "to_commit",
17
+ "find_worktree_git_dir",
18
+ ]
19
+
20
+ import os
21
+ import os.path as osp
22
+ from pathlib import Path
23
+ import stat
24
+ from string import digits
25
+
26
+ from gitdb.exc import BadName, BadObject
27
+
28
+ from git.cmd import Git
29
+ from git.exc import WorkTreeRepositoryUnsupported
30
+ from git.objects import Object
31
+ from git.refs import SymbolicReference
32
+ from git.util import cygpath, bin_to_hex, hex_to_bin
33
+
34
+ # Typing ----------------------------------------------------------------------
35
+
36
+ from typing import Optional, TYPE_CHECKING, Union, cast, overload
37
+
38
+ from git.types import AnyGitObject, Literal, PathLike
39
+
40
+ if TYPE_CHECKING:
41
+ from git.db import GitCmdObjectDB
42
+ from git.objects import Commit, TagObject
43
+ from git.refs.reference import Reference
44
+ from git.refs.tag import Tag
45
+
46
+ from .base import Repo
47
+
48
+ # ----------------------------------------------------------------------------
49
+
50
+
51
+ def touch(filename: str) -> str:
52
+ with open(filename, "ab"):
53
+ pass
54
+ return filename
55
+
56
+
57
+ def is_git_dir(d: PathLike) -> bool:
58
+ """This is taken from the git setup.c:is_git_directory function.
59
+
60
+ :raise git.exc.WorkTreeRepositoryUnsupported:
61
+ If it sees a worktree directory. It's quite hacky to do that here, but at least
62
+ clearly indicates that we don't support it. There is the unlikely danger to
63
+ throw if we see directories which just look like a worktree dir, but are none.
64
+ """
65
+ if osp.isdir(d):
66
+ if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
67
+ osp.join(d, "refs")
68
+ ):
69
+ headref = osp.join(d, "HEAD")
70
+ return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
71
+ elif (
72
+ osp.isfile(osp.join(d, "gitdir"))
73
+ and osp.isfile(osp.join(d, "commondir"))
74
+ and osp.isfile(osp.join(d, "gitfile"))
75
+ ):
76
+ raise WorkTreeRepositoryUnsupported(d)
77
+ return False
78
+
79
+
80
+ def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
81
+ """Search for a gitdir for this worktree."""
82
+ try:
83
+ statbuf = os.stat(dotgit)
84
+ except OSError:
85
+ return None
86
+ if not stat.S_ISREG(statbuf.st_mode):
87
+ return None
88
+
89
+ try:
90
+ lines = Path(dotgit).read_text().splitlines()
91
+ for key, value in [line.strip().split(": ") for line in lines]:
92
+ if key == "gitdir":
93
+ return value
94
+ except ValueError:
95
+ pass
96
+ return None
97
+
98
+
99
+ def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
100
+ """Search for a submodule repo."""
101
+ if is_git_dir(d):
102
+ return d
103
+
104
+ try:
105
+ with open(d) as fp:
106
+ content = fp.read().rstrip()
107
+ except IOError:
108
+ # It's probably not a file.
109
+ pass
110
+ else:
111
+ if content.startswith("gitdir: "):
112
+ path = content[8:]
113
+
114
+ if Git.is_cygwin():
115
+ # Cygwin creates submodules prefixed with `/cygdrive/...`.
116
+ # Cygwin git understands Cygwin paths much better than Windows ones.
117
+ # Also the Cygwin tests are assuming Cygwin paths.
118
+ path = cygpath(path)
119
+ if not osp.isabs(path):
120
+ path = osp.normpath(osp.join(osp.dirname(d), path))
121
+ return find_submodule_git_dir(path)
122
+ # END handle exception
123
+ return None
124
+
125
+
126
+ def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
127
+ """
128
+ :return:
129
+ Long hexadecimal sha1 from the given less than 40 byte hexsha, or ``None`` if no
130
+ candidate could be found.
131
+
132
+ :param hexsha:
133
+ hexsha with less than 40 bytes.
134
+ """
135
+ try:
136
+ return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha))
137
+ except BadObject:
138
+ return None
139
+ # END exception handling
140
+
141
+
142
+ @overload
143
+ def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ...
144
+
145
+
146
+ @overload
147
+ def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ...
148
+
149
+
150
+ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:
151
+ """
152
+ :return:
153
+ Object specified by the given name - hexshas (short and long) as well as
154
+ references are supported.
155
+
156
+ :param return_ref:
157
+ If ``True``, and name specifies a reference, we will return the reference
158
+ instead of the object. Otherwise it will raise :exc:`~gitdb.exc.BadObject` or
159
+ :exc:`~gitdb.exc.BadName`.
160
+ """
161
+ hexsha: Union[None, str, bytes] = None
162
+
163
+ # Is it a hexsha? Try the most common ones, which is 7 to 40.
164
+ if repo.re_hexsha_shortened.match(name):
165
+ if len(name) != 40:
166
+ # Find long sha for short sha.
167
+ hexsha = short_to_long(repo.odb, name)
168
+ else:
169
+ hexsha = name
170
+ # END handle short shas
171
+ # END find sha if it matches
172
+
173
+ # If we couldn't find an object for what seemed to be a short hexsha, try to find it
174
+ # as reference anyway, it could be named 'aaa' for instance.
175
+ if hexsha is None:
176
+ for base in (
177
+ "%s",
178
+ "refs/%s",
179
+ "refs/tags/%s",
180
+ "refs/heads/%s",
181
+ "refs/remotes/%s",
182
+ "refs/remotes/%s/HEAD",
183
+ ):
184
+ try:
185
+ hexsha = SymbolicReference.dereference_recursive(repo, base % name)
186
+ if return_ref:
187
+ return SymbolicReference(repo, base % name)
188
+ # END handle symbolic ref
189
+ break
190
+ except ValueError:
191
+ pass
192
+ # END for each base
193
+ # END handle hexsha
194
+
195
+ # Didn't find any ref, this is an error.
196
+ if return_ref:
197
+ raise BadObject("Couldn't find reference named %r" % name)
198
+ # END handle return ref
199
+
200
+ # Tried everything ? fail.
201
+ if hexsha is None:
202
+ raise BadName(name)
203
+ # END assert hexsha was found
204
+
205
+ return Object.new_from_sha(repo, hex_to_bin(hexsha))
206
+
207
+
208
+ def deref_tag(tag: "Tag") -> AnyGitObject:
209
+ """Recursively dereference a tag and return the resulting object."""
210
+ while True:
211
+ try:
212
+ tag = tag.object
213
+ except AttributeError:
214
+ break
215
+ # END dereference tag
216
+ return tag
217
+
218
+
219
+ def to_commit(obj: Object) -> "Commit":
220
+ """Convert the given object to a commit if possible and return it."""
221
+ if obj.type == "tag":
222
+ obj = deref_tag(obj)
223
+
224
+ if obj.type != "commit":
225
+ raise ValueError("Cannot convert object %r to type commit" % obj)
226
+ # END verify type
227
+ return obj
228
+
229
+
230
+ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
231
+ """Parse a revision string. Like :manpage:`git-rev-parse(1)`.
232
+
233
+ :return:
234
+ `~git.objects.base.Object` at the given revision.
235
+
236
+ This may be any type of git object:
237
+
238
+ * :class:`Commit <git.objects.commit.Commit>`
239
+ * :class:`TagObject <git.objects.tag.TagObject>`
240
+ * :class:`Tree <git.objects.tree.Tree>`
241
+ * :class:`Blob <git.objects.blob.Blob>`
242
+
243
+ :param rev:
244
+ :manpage:`git-rev-parse(1)`-compatible revision specification as string.
245
+ Please see :manpage:`git-rev-parse(1)` for details.
246
+
247
+ :raise gitdb.exc.BadObject:
248
+ If the given revision could not be found.
249
+
250
+ :raise ValueError:
251
+ If `rev` couldn't be parsed.
252
+
253
+ :raise IndexError:
254
+ If an invalid reflog index is specified.
255
+ """
256
+ # Are we in colon search mode?
257
+ if rev.startswith(":/"):
258
+ # Colon search mode
259
+ raise NotImplementedError("commit by message search (regex)")
260
+ # END handle search
261
+
262
+ obj: Optional[AnyGitObject] = None
263
+ ref = None
264
+ output_type = "commit"
265
+ start = 0
266
+ parsed_to = 0
267
+ lr = len(rev)
268
+ while start < lr:
269
+ if rev[start] not in "^~:@":
270
+ start += 1
271
+ continue
272
+ # END handle start
273
+
274
+ token = rev[start]
275
+
276
+ if obj is None:
277
+ # token is a rev name.
278
+ if start == 0:
279
+ ref = repo.head.ref
280
+ else:
281
+ if token == "@":
282
+ ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
283
+ else:
284
+ obj = name_to_object(repo, rev[:start])
285
+ # END handle token
286
+ # END handle refname
287
+ else:
288
+ if ref is not None:
289
+ obj = cast("Commit", ref.commit)
290
+ # END handle ref
291
+ # END initialize obj on first token
292
+
293
+ start += 1
294
+
295
+ # Try to parse {type}.
296
+ if start < lr and rev[start] == "{":
297
+ end = rev.find("}", start)
298
+ if end == -1:
299
+ raise ValueError("Missing closing brace to define type in %s" % rev)
300
+ output_type = rev[start + 1 : end] # Exclude brace.
301
+
302
+ # Handle type.
303
+ if output_type == "commit":
304
+ obj = cast("TagObject", obj)
305
+ if obj and obj.type == "tag":
306
+ obj = deref_tag(obj)
307
+ else:
308
+ # Cannot do anything for non-tags.
309
+ pass
310
+ # END handle tag
311
+ elif output_type == "tree":
312
+ try:
313
+ obj = cast(AnyGitObject, obj)
314
+ obj = to_commit(obj).tree
315
+ except (AttributeError, ValueError):
316
+ pass # Error raised later.
317
+ # END exception handling
318
+ elif output_type in ("", "blob"):
319
+ obj = cast("TagObject", obj)
320
+ if obj and obj.type == "tag":
321
+ obj = deref_tag(obj)
322
+ else:
323
+ # Cannot do anything for non-tags.
324
+ pass
325
+ # END handle tag
326
+ elif token == "@":
327
+ # try single int
328
+ assert ref is not None, "Require Reference to access reflog"
329
+ revlog_index = None
330
+ try:
331
+ # Transform reversed index into the format of our revlog.
332
+ revlog_index = -(int(output_type) + 1)
333
+ except ValueError as e:
334
+ # TODO: Try to parse the other date options, using parse_date maybe.
335
+ raise NotImplementedError("Support for additional @{...} modes not implemented") from e
336
+ # END handle revlog index
337
+
338
+ try:
339
+ entry = ref.log_entry(revlog_index)
340
+ except IndexError as e:
341
+ raise IndexError("Invalid revlog index: %i" % revlog_index) from e
342
+ # END handle index out of bound
343
+
344
+ obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
345
+
346
+ # Make it pass the following checks.
347
+ output_type = ""
348
+ else:
349
+ raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
350
+ # END handle output type
351
+
352
+ # Empty output types don't require any specific type, its just about
353
+ # dereferencing tags.
354
+ if output_type and obj and obj.type != output_type:
355
+ raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type))
356
+ # END verify output type
357
+
358
+ start = end + 1 # Skip brace.
359
+ parsed_to = start
360
+ continue
361
+ # END parse type
362
+
363
+ # Try to parse a number.
364
+ num = 0
365
+ if token != ":":
366
+ found_digit = False
367
+ while start < lr:
368
+ if rev[start] in digits:
369
+ num = num * 10 + int(rev[start])
370
+ start += 1
371
+ found_digit = True
372
+ else:
373
+ break
374
+ # END handle number
375
+ # END number parse loop
376
+
377
+ # No explicit number given, 1 is the default. It could be 0 though.
378
+ if not found_digit:
379
+ num = 1
380
+ # END set default num
381
+ # END number parsing only if non-blob mode
382
+
383
+ parsed_to = start
384
+ # Handle hierarchy walk.
385
+ try:
386
+ obj = cast(AnyGitObject, obj)
387
+ if token == "~":
388
+ obj = to_commit(obj)
389
+ for _ in range(num):
390
+ obj = obj.parents[0]
391
+ # END for each history item to walk
392
+ elif token == "^":
393
+ obj = to_commit(obj)
394
+ # Must be n'th parent.
395
+ if num:
396
+ obj = obj.parents[num - 1]
397
+ elif token == ":":
398
+ if obj.type != "tree":
399
+ obj = obj.tree
400
+ # END get tree type
401
+ obj = obj[rev[start:]]
402
+ parsed_to = lr
403
+ else:
404
+ raise ValueError("Invalid token: %r" % token)
405
+ # END end handle tag
406
+ except (IndexError, AttributeError) as e:
407
+ raise BadName(
408
+ f"Invalid revision spec '{rev}' - not enough parent commits to reach '{token}{int(num)}'"
409
+ ) from e
410
+ # END exception handling
411
+ # END parse loop
412
+
413
+ # Still no obj? It's probably a simple name.
414
+ if obj is None:
415
+ obj = name_to_object(repo, rev)
416
+ parsed_to = lr
417
+ # END handle simple name
418
+
419
+ if obj is None:
420
+ raise ValueError("Revision specifier could not be parsed: %s" % rev)
421
+
422
+ if parsed_to != lr:
423
+ raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to]))
424
+
425
+ return obj
.venv/lib/python3.11/site-packages/git/types.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ import os
5
+ import sys
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Dict,
10
+ List,
11
+ NoReturn,
12
+ Optional,
13
+ Sequence as Sequence,
14
+ Tuple,
15
+ TYPE_CHECKING,
16
+ Type,
17
+ TypeVar,
18
+ Union,
19
+ )
20
+ import warnings
21
+
22
+ if sys.version_info >= (3, 8):
23
+ from typing import (
24
+ Literal,
25
+ Protocol,
26
+ SupportsIndex as SupportsIndex,
27
+ TypedDict,
28
+ runtime_checkable,
29
+ )
30
+ else:
31
+ from typing_extensions import (
32
+ Literal,
33
+ Protocol,
34
+ SupportsIndex as SupportsIndex,
35
+ TypedDict,
36
+ runtime_checkable,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from git.objects import Commit, Tree, TagObject, Blob
41
+ from git.repo import Repo
42
+
43
+ PathLike = Union[str, "os.PathLike[str]"]
44
+ """A :class:`str` (Unicode) based file or directory path."""
45
+
46
+ TBD = Any
47
+ """Alias of :class:`~typing.Any`, when a type hint is meant to become more specific."""
48
+
49
+ _T = TypeVar("_T")
50
+ """Type variable used internally in GitPython."""
51
+
52
+ AnyGitObject = Union["Commit", "Tree", "TagObject", "Blob"]
53
+ """Union of the :class:`~git.objects.base.Object`-based types that represent actual git
54
+ object types.
55
+
56
+ As noted in :class:`~git.objects.base.Object`, which has further details, these are:
57
+
58
+ * :class:`Blob <git.objects.blob.Blob>`
59
+ * :class:`Tree <git.objects.tree.Tree>`
60
+ * :class:`Commit <git.objects.commit.Commit>`
61
+ * :class:`TagObject <git.objects.tag.TagObject>`
62
+
63
+ Those GitPython classes represent the four git object types, per
64
+ :manpage:`gitglossary(7)`:
65
+
66
+ * "blob": https://git-scm.com/docs/gitglossary#def_blob_object
67
+ * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
68
+ * "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
69
+ * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
70
+
71
+ For more general information on git objects and their types as git understands them:
72
+
73
+ * "object": https://git-scm.com/docs/gitglossary#def_object
74
+ * "object type": https://git-scm.com/docs/gitglossary#def_object_type
75
+
76
+ :note:
77
+ See also the :class:`Tree_ish` and :class:`Commit_ish` unions.
78
+ """
79
+
80
+ Tree_ish = Union["Commit", "Tree", "TagObject"]
81
+ """Union of :class:`~git.objects.base.Object`-based types that are typically tree-ish.
82
+
83
+ See :manpage:`gitglossary(7)` on "tree-ish":
84
+ https://git-scm.com/docs/gitglossary#def_tree-ish
85
+
86
+ :note:
87
+ :class:`~git.objects.tree.Tree` and :class:`~git.objects.commit.Commit` are the
88
+ classes whose instances are all tree-ish. This union includes them, but also
89
+ :class:`~git.objects.tag.TagObject`, only **most** of whose instances are tree-ish.
90
+ Whether a particular :class:`~git.objects.tag.TagObject` peels (recursively
91
+ dereferences) to a tree or commit, rather than a blob, can in general only be known
92
+ at runtime. In practice, git tag objects are nearly always used for tagging commits,
93
+ and such tags are tree-ish because commits are tree-ish.
94
+
95
+ :note:
96
+ See also the :class:`AnyGitObject` union of all four classes corresponding to git
97
+ object types.
98
+ """
99
+
100
+ Commit_ish = Union["Commit", "TagObject"]
101
+ """Union of :class:`~git.objects.base.Object`-based types that are typically commit-ish.
102
+
103
+ See :manpage:`gitglossary(7)` on "commit-ish":
104
+ https://git-scm.com/docs/gitglossary#def_commit-ish
105
+
106
+ :note:
107
+ :class:`~git.objects.commit.Commit` is the only class whose instances are all
108
+ commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also
109
+ :class:`~git.objects.tag.TagObject`, only **most** of whose instances are
110
+ commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels
111
+ (recursively dereferences) to a commit, rather than a tree or blob, can in general
112
+ only be known at runtime. In practice, git tag objects are nearly always used for
113
+ tagging commits, and such tags are of course commit-ish.
114
+
115
+ :note:
116
+ See also the :class:`AnyGitObject` union of all four classes corresponding to git
117
+ object types.
118
+ """
119
+
120
+ GitObjectTypeString = Literal["commit", "tag", "blob", "tree"]
121
+ """Literal strings identifying git object types and the
122
+ :class:`~git.objects.base.Object`-based types that represent them.
123
+
124
+ See the :attr:`Object.type <git.objects.base.Object.type>` attribute. These are its
125
+ values in :class:`~git.objects.base.Object` subclasses that represent git objects. These
126
+ literals therefore correspond to the types in the :class:`AnyGitObject` union.
127
+
128
+ These are the same strings git itself uses to identify its four object types.
129
+ See :manpage:`gitglossary(7)` on "object type":
130
+ https://git-scm.com/docs/gitglossary#def_object_type
131
+ """
132
+
133
+ Lit_commit_ish: Type[Literal["commit", "tag"]]
134
+ """Deprecated. Type of literal strings identifying typically-commitish git object types.
135
+
136
+ Prior to a bugfix, this type had been defined more broadly. Any usage is in practice
137
+ ambiguous and likely to be incorrect. This type has therefore been made a static type
138
+ error to appear in annotations. It is preserved, with a deprecated status, to avoid
139
+ introducing runtime errors in code that refers to it, but it should not be used.
140
+
141
+ Instead of this type:
142
+
143
+ * For the type of the string literals associated with :class:`Commit_ish`, use
144
+ ``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
145
+ this type as currently defined (but usable in statically checked type annotations).
146
+
147
+ * For the type of all four string literals associated with :class:`AnyGitObject`, use
148
+ :class:`GitObjectTypeString`. That is equivalent to the old definition of this type
149
+ prior to the bugfix (and is also usable in statically checked type annotations).
150
+ """
151
+
152
+
153
+ def _getattr(name: str) -> Any:
154
+ if name != "Lit_commit_ish":
155
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
156
+
157
+ warnings.warn(
158
+ "Lit_commit_ish is deprecated. It is currently defined as "
159
+ '`Literal["commit", "tag"]`, which should be used in its place if desired. It '
160
+ 'had previously been defined as `Literal["commit", "tag", "blob", "tree"]`, '
161
+ "covering all four git object type strings including those that are never "
162
+ "commit-ish. For that, use the GitObjectTypeString type instead.",
163
+ DeprecationWarning,
164
+ stacklevel=2,
165
+ )
166
+ return Literal["commit", "tag"]
167
+
168
+
169
+ if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
170
+ __getattr__ = _getattr
171
+
172
+
173
+ def __dir__() -> List[str]:
174
+ return [*globals(), "Lit_commit_ish"]
175
+
176
+
177
+ # Config_levels ---------------------------------------------------------
178
+
179
+ Lit_config_levels = Literal["system", "global", "user", "repository"]
180
+ """Type of literal strings naming git configuration levels.
181
+
182
+ These strings relate to which file a git configuration variable is in.
183
+ """
184
+
185
+ ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
186
+ """Static type of a tuple of the four strings representing configuration levels."""
187
+
188
+ # Progress parameter type alias -----------------------------------------
189
+
190
+ CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]
191
+ """General type of a function or other callable used as a progress reporter for cloning.
192
+
193
+ This is the type of a function or other callable that reports the progress of a clone,
194
+ when passed as a ``progress`` argument to :meth:`Repo.clone <git.repo.base.Repo.clone>`
195
+ or :meth:`Repo.clone_from <git.repo.base.Repo.clone_from>`.
196
+
197
+ :note:
198
+ Those :meth:`~git.repo.base.Repo.clone` and :meth:`~git.repo.base.Repo.clone_from`
199
+ methods also accept :meth:`~git.util.RemoteProgress` instances, including instances
200
+ of its :meth:`~git.util.CallableRemoteProgress` subclass.
201
+
202
+ :note:
203
+ Unlike objects that match this type, :meth:`~git.util.RemoteProgress` instances are
204
+ not directly callable, not even when they are instances of
205
+ :meth:`~git.util.CallableRemoteProgress`, which wraps a callable and forwards
206
+ information to it but is not itself callable.
207
+
208
+ :note:
209
+ This type also allows ``None``, for cloning without reporting progress.
210
+ """
211
+
212
+ # -----------------------------------------------------------------------------------
213
+
214
+
215
+ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None:
216
+ """For use in exhaustive checking of a literal or enum in if/else chains.
217
+
218
+ A call to this function should only be reached if not all members are handled, or if
219
+ an attempt is made to pass non-members through the chain.
220
+
221
+ :param inp:
222
+ If all members are handled, the argument for `inp` will have the
223
+ :class:`~typing.Never`/:class:`~typing.NoReturn` type.
224
+ Otherwise, the type will mismatch and cause a mypy error.
225
+
226
+ :param raise_error:
227
+ If ``True``, will also raise :exc:`ValueError` with a general
228
+ "unhandled literal" message, or the exception object passed as `exc`.
229
+
230
+ :param exc:
231
+ It not ``None``, this should be an already-constructed exception object, to be
232
+ raised if `raise_error` is ``True``.
233
+ """
234
+ if raise_error:
235
+ if exc is None:
236
+ raise ValueError(f"An unhandled literal ({inp!r}) in an if/else chain was found")
237
+ else:
238
+ raise exc
239
+
240
+
241
+ class Files_TD(TypedDict):
242
+ """Dictionary with stat counts for the diff of a particular file.
243
+
244
+ For the :class:`~git.util.Stats.files` attribute of :class:`~git.util.Stats`
245
+ objects.
246
+ """
247
+
248
+ insertions: int
249
+ deletions: int
250
+ lines: int
251
+ change_type: str
252
+
253
+
254
+ class Total_TD(TypedDict):
255
+ """Dictionary with total stats from any number of files.
256
+
257
+ For the :class:`~git.util.Stats.total` attribute of :class:`~git.util.Stats`
258
+ objects.
259
+ """
260
+
261
+ insertions: int
262
+ deletions: int
263
+ lines: int
264
+ files: int
265
+
266
+
267
+ class HSH_TD(TypedDict):
268
+ """Dictionary carrying the same information as a :class:`~git.util.Stats` object."""
269
+
270
+ total: Total_TD
271
+ files: Dict[PathLike, Files_TD]
272
+
273
+
274
+ @runtime_checkable
275
+ class Has_Repo(Protocol):
276
+ """Protocol for having a :attr:`repo` attribute, the repository to operate on."""
277
+
278
+ repo: "Repo"
279
+
280
+
281
+ @runtime_checkable
282
+ class Has_id_attribute(Protocol):
283
+ """Protocol for having :attr:`_id_attribute_` used in iteration and traversal."""
284
+
285
+ _id_attribute_: str
.venv/lib/python3.11/site-packages/git/util.py ADDED
@@ -0,0 +1,1350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ import sys
7
+
8
+ __all__ = [
9
+ "stream_copy",
10
+ "join_path",
11
+ "to_native_path_linux",
12
+ "join_path_native",
13
+ "Stats",
14
+ "IndexFileSHA1Writer",
15
+ "IterableObj",
16
+ "IterableList",
17
+ "BlockingLockFile",
18
+ "LockFile",
19
+ "Actor",
20
+ "get_user_id",
21
+ "assure_directory_exists",
22
+ "RemoteProgress",
23
+ "CallableRemoteProgress",
24
+ "rmtree",
25
+ "unbare_repo",
26
+ "HIDE_WINDOWS_KNOWN_ERRORS",
27
+ ]
28
+
29
+ if sys.platform == "win32":
30
+ __all__.append("to_native_path_windows")
31
+
32
+ from abc import abstractmethod
33
+ import contextlib
34
+ from functools import wraps
35
+ import getpass
36
+ import logging
37
+ import os
38
+ import os.path as osp
39
+ import pathlib
40
+ import platform
41
+ import re
42
+ import shutil
43
+ import stat
44
+ import subprocess
45
+ import time
46
+ from urllib.parse import urlsplit, urlunsplit
47
+ import warnings
48
+
49
+ # NOTE: Unused imports can be improved now that CI testing has fully resumed. Some of
50
+ # these be used indirectly through other GitPython modules, which avoids having to write
51
+ # gitdb all the time in their imports. They are not in __all__, at least currently,
52
+ # because they could be removed or changed at any time, and so should not be considered
53
+ # conceptually public to code outside GitPython. Linters of course do not like it.
54
+ from gitdb.util import (
55
+ LazyMixin, # noqa: F401
56
+ LockedFD, # noqa: F401
57
+ bin_to_hex, # noqa: F401
58
+ file_contents_ro, # noqa: F401
59
+ file_contents_ro_filepath, # noqa: F401
60
+ hex_to_bin, # noqa: F401
61
+ make_sha,
62
+ to_bin_sha, # noqa: F401
63
+ to_hex_sha, # noqa: F401
64
+ )
65
+
66
+ # typing ---------------------------------------------------------
67
+
68
+ from typing import (
69
+ Any,
70
+ AnyStr,
71
+ BinaryIO,
72
+ Callable,
73
+ Dict,
74
+ Generator,
75
+ IO,
76
+ Iterator,
77
+ List,
78
+ Optional,
79
+ Pattern,
80
+ Sequence,
81
+ Tuple,
82
+ TYPE_CHECKING,
83
+ TypeVar,
84
+ Union,
85
+ cast,
86
+ overload,
87
+ )
88
+
89
+ if TYPE_CHECKING:
90
+ from git.cmd import Git
91
+ from git.config import GitConfigParser, SectionConstraint
92
+ from git.remote import Remote
93
+ from git.repo.base import Repo
94
+
95
+ from git.types import (
96
+ Files_TD,
97
+ Has_id_attribute,
98
+ HSH_TD,
99
+ Literal,
100
+ PathLike,
101
+ Protocol,
102
+ SupportsIndex,
103
+ Total_TD,
104
+ runtime_checkable,
105
+ )
106
+
107
+ # ---------------------------------------------------------------------
108
+
109
+ T_IterableObj = TypeVar("T_IterableObj", bound=Union["IterableObj", "Has_id_attribute"], covariant=True)
110
+ # So IterableList[Head] is subtype of IterableList[IterableObj].
111
+
112
+ _logger = logging.getLogger(__name__)
113
+
114
+
115
+ def _read_env_flag(name: str, default: bool) -> bool:
116
+ """Read a boolean flag from an environment variable.
117
+
118
+ :return:
119
+ The flag, or the `default` value if absent or ambiguous.
120
+ """
121
+ try:
122
+ value = os.environ[name]
123
+ except KeyError:
124
+ return default
125
+
126
+ _logger.warning(
127
+ "The %s environment variable is deprecated. Its effect has never been documented and changes without warning.",
128
+ name,
129
+ )
130
+
131
+ adjusted_value = value.strip().lower()
132
+
133
+ if adjusted_value in {"", "0", "false", "no"}:
134
+ return False
135
+ if adjusted_value in {"1", "true", "yes"}:
136
+ return True
137
+ _logger.warning("%s has unrecognized value %r, treating as %r.", name, value, default)
138
+ return default
139
+
140
+
141
+ def _read_win_env_flag(name: str, default: bool) -> bool:
142
+ """Read a boolean flag from an environment variable on Windows.
143
+
144
+ :return:
145
+ On Windows, the flag, or the `default` value if absent or ambiguous.
146
+ On all other operating systems, ``False``.
147
+
148
+ :note:
149
+ This only accesses the environment on Windows.
150
+ """
151
+ return sys.platform == "win32" and _read_env_flag(name, default)
152
+
153
+
154
+ #: We need an easy way to see if Appveyor TCs start failing,
155
+ #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
156
+ #: till then, we wish to hide them.
157
+ HIDE_WINDOWS_KNOWN_ERRORS = _read_win_env_flag("HIDE_WINDOWS_KNOWN_ERRORS", True)
158
+ HIDE_WINDOWS_FREEZE_ERRORS = _read_win_env_flag("HIDE_WINDOWS_FREEZE_ERRORS", True)
159
+
160
+ # { Utility Methods
161
+
162
+ T = TypeVar("T")
163
+
164
+
165
+ def unbare_repo(func: Callable[..., T]) -> Callable[..., T]:
166
+ """Methods with this decorator raise :exc:`~git.exc.InvalidGitRepositoryError` if
167
+ they encounter a bare repository."""
168
+
169
+ from .exc import InvalidGitRepositoryError
170
+
171
+ @wraps(func)
172
+ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T:
173
+ if self.repo.bare:
174
+ raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__)
175
+ # END bare method
176
+ return func(self, *args, **kwargs)
177
+
178
+ # END wrapper
179
+
180
+ return wrapper
181
+
182
+
183
+ @contextlib.contextmanager
184
+ def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
185
+ """Context manager to temporarily change directory.
186
+
187
+ This is similar to :func:`contextlib.chdir` introduced in Python 3.11, but the
188
+ context manager object returned by a single call to this function is not reentrant.
189
+ """
190
+ old_dir = os.getcwd()
191
+ os.chdir(new_dir)
192
+ try:
193
+ yield new_dir
194
+ finally:
195
+ os.chdir(old_dir)
196
+
197
+
198
+ @contextlib.contextmanager
199
+ def patch_env(name: str, value: str) -> Generator[None, None, None]:
200
+ """Context manager to temporarily patch an environment variable."""
201
+ old_value = os.getenv(name)
202
+ os.environ[name] = value
203
+ try:
204
+ yield
205
+ finally:
206
+ if old_value is None:
207
+ del os.environ[name]
208
+ else:
209
+ os.environ[name] = old_value
210
+
211
+
212
+ def rmtree(path: PathLike) -> None:
213
+ """Remove the given directory tree recursively.
214
+
215
+ :note:
216
+ We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that
217
+ couldn't be deleted are read-only. Windows will not remove them in that case.
218
+ """
219
+
220
+ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
221
+ """Callback for :func:`shutil.rmtree`.
222
+
223
+ This works as either a ``onexc`` or ``onerror`` style callback.
224
+ """
225
+ # Is the error an access error?
226
+ os.chmod(path, stat.S_IWUSR)
227
+
228
+ try:
229
+ function(path)
230
+ except PermissionError as ex:
231
+ if HIDE_WINDOWS_KNOWN_ERRORS:
232
+ from unittest import SkipTest
233
+
234
+ raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex
235
+ raise
236
+
237
+ if sys.platform != "win32":
238
+ shutil.rmtree(path)
239
+ elif sys.version_info >= (3, 12):
240
+ shutil.rmtree(path, onexc=handler)
241
+ else:
242
+ shutil.rmtree(path, onerror=handler)
243
+
244
+
245
+ def rmfile(path: PathLike) -> None:
246
+ """Ensure file deleted also on *Windows* where read-only files need special
247
+ treatment."""
248
+ if osp.isfile(path):
249
+ if sys.platform == "win32":
250
+ os.chmod(path, 0o777)
251
+ os.remove(path)
252
+
253
+
254
+ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * 1024) -> int:
255
+ """Copy all data from the `source` stream into the `destination` stream in chunks
256
+ of size `chunk_size`.
257
+
258
+ :return:
259
+ Number of bytes written
260
+ """
261
+ br = 0
262
+ while True:
263
+ chunk = source.read(chunk_size)
264
+ destination.write(chunk)
265
+ br += len(chunk)
266
+ if len(chunk) < chunk_size:
267
+ break
268
+ # END reading output stream
269
+ return br
270
+
271
+
272
+ def join_path(a: PathLike, *p: PathLike) -> PathLike:
273
+ R"""Join path tokens together similar to osp.join, but always use ``/`` instead of
274
+ possibly ``\`` on Windows."""
275
+ path = str(a)
276
+ for b in p:
277
+ b = str(b)
278
+ if not b:
279
+ continue
280
+ if b.startswith("/"):
281
+ path += b[1:]
282
+ elif path == "" or path.endswith("/"):
283
+ path += b
284
+ else:
285
+ path += "/" + b
286
+ # END for each path token to add
287
+ return path
288
+
289
+
290
+ if sys.platform == "win32":
291
+
292
+ def to_native_path_windows(path: PathLike) -> PathLike:
293
+ path = str(path)
294
+ return path.replace("/", "\\")
295
+
296
+ def to_native_path_linux(path: PathLike) -> str:
297
+ path = str(path)
298
+ return path.replace("\\", "/")
299
+
300
+ to_native_path = to_native_path_windows
301
+ else:
302
+ # No need for any work on Linux.
303
+ def to_native_path_linux(path: PathLike) -> str:
304
+ return str(path)
305
+
306
+ to_native_path = to_native_path_linux
307
+
308
+
309
+ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
310
+ R"""Like :func:`join_path`, but makes sure an OS native path is returned.
311
+
312
+ This is only needed to play it safe on Windows and to ensure nice paths that only
313
+ use ``\``.
314
+ """
315
+ return to_native_path(join_path(a, *p))
316
+
317
+
318
+ def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
319
+ """Make sure that the directory pointed to by path exists.
320
+
321
+ :param is_file:
322
+ If ``True``, `path` is assumed to be a file and handled correctly.
323
+ Otherwise it must be a directory.
324
+
325
+ :return:
326
+ ``True`` if the directory was created, ``False`` if it already existed.
327
+ """
328
+ if is_file:
329
+ path = osp.dirname(path)
330
+ # END handle file
331
+ if not osp.isdir(path):
332
+ os.makedirs(path, exist_ok=True)
333
+ return True
334
+ return False
335
+
336
+
337
+ def _get_exe_extensions() -> Sequence[str]:
338
+ PATHEXT = os.environ.get("PATHEXT", None)
339
+ if PATHEXT:
340
+ return tuple(p.upper() for p in PATHEXT.split(os.pathsep))
341
+ elif sys.platform == "win32":
342
+ return (".BAT", ".COM", ".EXE")
343
+ else:
344
+ return ()
345
+
346
+
347
+ def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
348
+ """Perform a path search to assist :func:`is_cygwin_git`.
349
+
350
+ This is not robust for general use. It is an implementation detail of
351
+ :func:`is_cygwin_git`. When a search following all shell rules is needed,
352
+ :func:`shutil.which` can be used instead.
353
+
354
+ :note:
355
+ Neither this function nor :func:`shutil.which` will predict the effect of an
356
+ executable search on a native Windows system due to a :class:`subprocess.Popen`
357
+ call without ``shell=True``, because shell and non-shell executable search on
358
+ Windows differ considerably.
359
+ """
360
+ # From: http://stackoverflow.com/a/377028/548792
361
+ winprog_exts = _get_exe_extensions()
362
+
363
+ def is_exec(fpath: str) -> bool:
364
+ return (
365
+ osp.isfile(fpath)
366
+ and os.access(fpath, os.X_OK)
367
+ and (
368
+ sys.platform != "win32" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts)
369
+ )
370
+ )
371
+
372
+ progs = []
373
+ if not path:
374
+ path = os.environ["PATH"]
375
+ for folder in str(path).split(os.pathsep):
376
+ folder = folder.strip('"')
377
+ if folder:
378
+ exe_path = osp.join(folder, program)
379
+ for f in [exe_path] + ["%s%s" % (exe_path, e) for e in winprog_exts]:
380
+ if is_exec(f):
381
+ progs.append(f)
382
+ return progs
383
+
384
+
385
+ def _cygexpath(drive: Optional[str], path: str) -> str:
386
+ if osp.isabs(path) and not drive:
387
+ # Invoked from `cygpath()` directly with `D:Apps\123`?
388
+ # It's an error, leave it alone just slashes)
389
+ p = path # convert to str if AnyPath given
390
+ else:
391
+ p = path and osp.normpath(osp.expandvars(osp.expanduser(path)))
392
+ if osp.isabs(p):
393
+ if drive:
394
+ # Confusing, maybe a remote system should expand vars.
395
+ p = path
396
+ else:
397
+ p = cygpath(p)
398
+ elif drive:
399
+ p = "/proc/cygdrive/%s/%s" % (drive.lower(), p)
400
+ p_str = str(p) # ensure it is a str and not AnyPath
401
+ return p_str.replace("\\", "/")
402
+
403
+
404
+ _cygpath_parsers: Tuple[Tuple[Pattern[str], Callable, bool], ...] = (
405
+ # See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
406
+ # and: https://www.cygwin.com/cygwin-ug-net/using.html#unc-paths
407
+ (
408
+ re.compile(r"\\\\\?\\UNC\\([^\\]+)\\([^\\]+)(?:\\(.*))?"),
409
+ (lambda server, share, rest_path: "//%s/%s/%s" % (server, share, rest_path.replace("\\", "/"))),
410
+ False,
411
+ ),
412
+ (re.compile(r"\\\\\?\\(\w):[/\\](.*)"), (_cygexpath), False),
413
+ (re.compile(r"(\w):[/\\](.*)"), (_cygexpath), False),
414
+ (re.compile(r"file:(.*)", re.I), (lambda rest_path: rest_path), True),
415
+ (re.compile(r"(\w{2,}:.*)"), (lambda url: url), False), # remote URL, do nothing
416
+ )
417
+
418
+
419
+ def cygpath(path: str) -> str:
420
+ """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
421
+ path = str(path) # Ensure is str and not AnyPath.
422
+ # Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs?
423
+ if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")):
424
+ for regex, parser, recurse in _cygpath_parsers:
425
+ match = regex.match(path)
426
+ if match:
427
+ path = parser(*match.groups())
428
+ if recurse:
429
+ path = cygpath(path)
430
+ break
431
+ else:
432
+ path = _cygexpath(None, path)
433
+
434
+ return path
435
+
436
+
437
+ _decygpath_regex = re.compile(r"(?:/proc)?/cygdrive/(\w)(/.*)?")
438
+
439
+
440
+ def decygpath(path: PathLike) -> str:
441
+ path = str(path)
442
+ m = _decygpath_regex.match(path)
443
+ if m:
444
+ drive, rest_path = m.groups()
445
+ path = "%s:%s" % (drive.upper(), rest_path or "")
446
+
447
+ return path.replace("/", "\\")
448
+
449
+
450
+ #: Store boolean flags denoting if a specific Git executable
451
+ #: is from a Cygwin installation (since `cache_lru()` unsupported on PY2).
452
+ _is_cygwin_cache: Dict[str, Optional[bool]] = {}
453
+
454
+
455
+ def _is_cygwin_git(git_executable: str) -> bool:
456
+ is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool]
457
+ if is_cygwin is None:
458
+ is_cygwin = False
459
+ try:
460
+ git_dir = osp.dirname(git_executable)
461
+ if not git_dir:
462
+ res = py_where(git_executable)
463
+ git_dir = osp.dirname(res[0]) if res else ""
464
+
465
+ # Just a name given, not a real path.
466
+ uname_cmd = osp.join(git_dir, "uname")
467
+
468
+ if not (pathlib.Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
469
+ _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
470
+ _is_cygwin_cache[git_executable] = is_cygwin
471
+ return is_cygwin
472
+
473
+ process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
474
+ uname_out, _ = process.communicate()
475
+ # retcode = process.poll()
476
+ is_cygwin = "CYGWIN" in uname_out
477
+ except Exception as ex:
478
+ _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
479
+ _is_cygwin_cache[git_executable] = is_cygwin
480
+
481
+ return is_cygwin
482
+
483
+
484
+ @overload
485
+ def is_cygwin_git(git_executable: None) -> Literal[False]: ...
486
+
487
+
488
+ @overload
489
+ def is_cygwin_git(git_executable: PathLike) -> bool: ...
490
+
491
+
492
+ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
493
+ # TODO: when py3.7 support is dropped, use the new interpolation f"{variable=}"
494
+ _logger.debug(f"sys.platform={sys.platform!r}, git_executable={git_executable!r}")
495
+ if sys.platform != "cygwin":
496
+ return False
497
+ elif git_executable is None:
498
+ return False
499
+ else:
500
+ return _is_cygwin_git(str(git_executable))
501
+
502
+
503
+ def get_user_id() -> str:
504
+ """:return: String identifying the currently active system user as ``name@node``"""
505
+ return "%s@%s" % (getpass.getuser(), platform.node())
506
+
507
+
508
+ def finalize_process(proc: Union[subprocess.Popen, "Git.AutoInterrupt"], **kwargs: Any) -> None:
509
+ """Wait for the process (clone, fetch, pull or push) and handle its errors
510
+ accordingly."""
511
+ # TODO: No close proc-streams??
512
+ proc.wait(**kwargs)
513
+
514
+
515
+ @overload
516
+ def expand_path(p: None, expand_vars: bool = ...) -> None: ...
517
+
518
+
519
+ @overload
520
+ def expand_path(p: PathLike, expand_vars: bool = ...) -> str:
521
+ # TODO: Support for Python 3.5 has been dropped, so these overloads can be improved.
522
+ ...
523
+
524
+
525
+ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[PathLike]:
526
+ if isinstance(p, pathlib.Path):
527
+ return p.resolve()
528
+ try:
529
+ p = osp.expanduser(p) # type: ignore[arg-type]
530
+ if expand_vars:
531
+ p = osp.expandvars(p)
532
+ return osp.normpath(osp.abspath(p))
533
+ except Exception:
534
+ return None
535
+
536
+
537
+ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
538
+ """Parse any command line argument and if one of the elements is an URL with a
539
+ username and/or password, replace them by stars (in-place).
540
+
541
+ If nothing is found, this just returns the command line as-is.
542
+
543
+ This should be used for every log line that print a command line, as well as
544
+ exception messages.
545
+ """
546
+ new_cmdline = []
547
+ for index, to_parse in enumerate(cmdline):
548
+ new_cmdline.append(to_parse)
549
+ try:
550
+ url = urlsplit(to_parse)
551
+ # Remove password from the URL if present.
552
+ if url.password is None and url.username is None:
553
+ continue
554
+
555
+ if url.password is not None:
556
+ url = url._replace(netloc=url.netloc.replace(url.password, "*****"))
557
+ if url.username is not None:
558
+ url = url._replace(netloc=url.netloc.replace(url.username, "*****"))
559
+ new_cmdline[index] = urlunsplit(url)
560
+ except ValueError:
561
+ # This is not a valid URL.
562
+ continue
563
+ return new_cmdline
564
+
565
+
566
+ # } END utilities
567
+
568
+ # { Classes
569
+
570
+
571
+ class RemoteProgress:
572
+ """Handler providing an interface to parse progress information emitted by
573
+ :manpage:`git-push(1)` and :manpage:`git-fetch(1)` and to dispatch callbacks
574
+ allowing subclasses to react to the progress."""
575
+
576
+ _num_op_codes: int = 9
577
+ (
578
+ BEGIN,
579
+ END,
580
+ COUNTING,
581
+ COMPRESSING,
582
+ WRITING,
583
+ RECEIVING,
584
+ RESOLVING,
585
+ FINDING_SOURCES,
586
+ CHECKING_OUT,
587
+ ) = [1 << x for x in range(_num_op_codes)]
588
+ STAGE_MASK = BEGIN | END
589
+ OP_MASK = ~STAGE_MASK
590
+
591
+ DONE_TOKEN = "done."
592
+ TOKEN_SEPARATOR = ", "
593
+
594
+ __slots__ = (
595
+ "_cur_line",
596
+ "_seen_ops",
597
+ "error_lines", # Lines that started with 'error:' or 'fatal:'.
598
+ "other_lines", # Lines not denoting progress (i.e.g. push-infos).
599
+ )
600
+ re_op_absolute = re.compile(r"(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
601
+ re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")
602
+
603
+ def __init__(self) -> None:
604
+ self._seen_ops: List[int] = []
605
+ self._cur_line: Optional[str] = None
606
+ self.error_lines: List[str] = []
607
+ self.other_lines: List[str] = []
608
+
609
+ def _parse_progress_line(self, line: AnyStr) -> None:
610
+ """Parse progress information from the given line as retrieved by
611
+ :manpage:`git-push(1)` or :manpage:`git-fetch(1)`.
612
+
613
+ - Lines that do not contain progress info are stored in :attr:`other_lines`.
614
+ - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``)
615
+ are stored in :attr:`error_lines`.
616
+ """
617
+ # handle
618
+ # Counting objects: 4, done.
619
+ # Compressing objects: 50% (1/2)
620
+ # Compressing objects: 100% (2/2)
621
+ # Compressing objects: 100% (2/2), done.
622
+ if isinstance(line, bytes): # mypy argues about ternary assignment.
623
+ line_str = line.decode("utf-8")
624
+ else:
625
+ line_str = line
626
+ self._cur_line = line_str
627
+
628
+ if self._cur_line.startswith(("error:", "fatal:")):
629
+ self.error_lines.append(self._cur_line)
630
+ return
631
+
632
+ cur_count, max_count = None, None
633
+ match = self.re_op_relative.match(line_str)
634
+ if match is None:
635
+ match = self.re_op_absolute.match(line_str)
636
+
637
+ if not match:
638
+ self.line_dropped(line_str)
639
+ self.other_lines.append(line_str)
640
+ return
641
+ # END could not get match
642
+
643
+ op_code = 0
644
+ _remote, op_name, _percent, cur_count, max_count, message = match.groups()
645
+
646
+ # Get operation ID.
647
+ if op_name == "Counting objects":
648
+ op_code |= self.COUNTING
649
+ elif op_name == "Compressing objects":
650
+ op_code |= self.COMPRESSING
651
+ elif op_name == "Writing objects":
652
+ op_code |= self.WRITING
653
+ elif op_name == "Receiving objects":
654
+ op_code |= self.RECEIVING
655
+ elif op_name == "Resolving deltas":
656
+ op_code |= self.RESOLVING
657
+ elif op_name == "Finding sources":
658
+ op_code |= self.FINDING_SOURCES
659
+ elif op_name == "Checking out files":
660
+ op_code |= self.CHECKING_OUT
661
+ else:
662
+ # Note: On Windows it can happen that partial lines are sent.
663
+ # Hence we get something like "CompreReceiving objects", which is
664
+ # a blend of "Compressing objects" and "Receiving objects".
665
+ # This can't really be prevented, so we drop the line verbosely
666
+ # to make sure we get informed in case the process spits out new
667
+ # commands at some point.
668
+ self.line_dropped(line_str)
669
+ # Note: Don't add this line to the other lines, as we have to silently
670
+ # drop it.
671
+ return
672
+ # END handle op code
673
+
674
+ # Figure out stage.
675
+ if op_code not in self._seen_ops:
676
+ self._seen_ops.append(op_code)
677
+ op_code |= self.BEGIN
678
+ # END begin opcode
679
+
680
+ if message is None:
681
+ message = ""
682
+ # END message handling
683
+
684
+ message = message.strip()
685
+ if message.endswith(self.DONE_TOKEN):
686
+ op_code |= self.END
687
+ message = message[: -len(self.DONE_TOKEN)]
688
+ # END end message handling
689
+ message = message.strip(self.TOKEN_SEPARATOR)
690
+
691
+ self.update(
692
+ op_code,
693
+ cur_count and float(cur_count),
694
+ max_count and float(max_count),
695
+ message,
696
+ )
697
+
698
+ def new_message_handler(self) -> Callable[[str], None]:
699
+ """
700
+ :return:
701
+ A progress handler suitable for :func:`~git.cmd.handle_process_output`,
702
+ passing lines on to this progress handler in a suitable format.
703
+ """
704
+
705
+ def handler(line: AnyStr) -> None:
706
+ return self._parse_progress_line(line.rstrip())
707
+
708
+ # END handler
709
+
710
+ return handler
711
+
712
+ def line_dropped(self, line: str) -> None:
713
+ """Called whenever a line could not be understood and was therefore dropped."""
714
+ pass
715
+
716
+ def update(
717
+ self,
718
+ op_code: int,
719
+ cur_count: Union[str, float],
720
+ max_count: Union[str, float, None] = None,
721
+ message: str = "",
722
+ ) -> None:
723
+ """Called whenever the progress changes.
724
+
725
+ :param op_code:
726
+ Integer allowing to be compared against Operation IDs and stage IDs.
727
+
728
+ Stage IDs are :const:`BEGIN` and :const:`END`. :const:`BEGIN` will only be
729
+ set once for each Operation ID as well as :const:`END`. It may be that
730
+ :const:`BEGIN` and :const:`END` are set at once in case only one progress
731
+ message was emitted due to the speed of the operation. Between
732
+ :const:`BEGIN` and :const:`END`, none of these flags will be set.
733
+
734
+ Operation IDs are all held within the :const:`OP_MASK`. Only one Operation
735
+ ID will be active per call.
736
+
737
+ :param cur_count:
738
+ Current absolute count of items.
739
+
740
+ :param max_count:
741
+ The maximum count of items we expect. It may be ``None`` in case there is no
742
+ maximum number of items or if it is (yet) unknown.
743
+
744
+ :param message:
745
+ In case of the :const:`WRITING` operation, it contains the amount of bytes
746
+ transferred. It may possibly be used for other purposes as well.
747
+
748
+ :note:
749
+ You may read the contents of the current line in
750
+ :attr:`self._cur_line <_cur_line>`.
751
+ """
752
+ pass
753
+
754
+
755
+ class CallableRemoteProgress(RemoteProgress):
756
+ """A :class:`RemoteProgress` implementation forwarding updates to any callable.
757
+
758
+ :note:
759
+ Like direct instances of :class:`RemoteProgress`, instances of this
760
+ :class:`CallableRemoteProgress` class are not themselves directly callable.
761
+ Rather, instances of this class wrap a callable and forward to it. This should
762
+ therefore not be confused with :class:`git.types.CallableProgress`.
763
+ """
764
+
765
+ __slots__ = ("_callable",)
766
+
767
+ def __init__(self, fn: Callable) -> None:
768
+ self._callable = fn
769
+ super().__init__()
770
+
771
+ def update(self, *args: Any, **kwargs: Any) -> None:
772
+ self._callable(*args, **kwargs)
773
+
774
+
775
+ class Actor:
776
+ """Actors hold information about a person acting on the repository. They can be
777
+ committers and authors or anything with a name and an email as mentioned in the git
778
+ log entries."""
779
+
780
+ # PRECOMPILED REGEX
781
+ name_only_regex = re.compile(r"<(.*)>")
782
+ name_email_regex = re.compile(r"(.*) <(.*?)>")
783
+
784
+ # ENVIRONMENT VARIABLES
785
+ # These are read when creating new commits.
786
+ env_author_name = "GIT_AUTHOR_NAME"
787
+ env_author_email = "GIT_AUTHOR_EMAIL"
788
+ env_committer_name = "GIT_COMMITTER_NAME"
789
+ env_committer_email = "GIT_COMMITTER_EMAIL"
790
+
791
+ # CONFIGURATION KEYS
792
+ conf_name = "name"
793
+ conf_email = "email"
794
+
795
+ __slots__ = ("name", "email")
796
+
797
+ def __init__(self, name: Optional[str], email: Optional[str]) -> None:
798
+ self.name = name
799
+ self.email = email
800
+
801
+ def __eq__(self, other: Any) -> bool:
802
+ return self.name == other.name and self.email == other.email
803
+
804
+ def __ne__(self, other: Any) -> bool:
805
+ return not (self == other)
806
+
807
+ def __hash__(self) -> int:
808
+ return hash((self.name, self.email))
809
+
810
+ def __str__(self) -> str:
811
+ return self.name if self.name else ""
812
+
813
+ def __repr__(self) -> str:
814
+ return '<git.Actor "%s <%s>">' % (self.name, self.email)
815
+
816
+ @classmethod
817
+ def _from_string(cls, string: str) -> "Actor":
818
+ """Create an :class:`Actor` from a string.
819
+
820
+ :param string:
821
+ The string, which is expected to be in regular git format::
822
+
823
+ John Doe <jdoe@example.com>
824
+
825
+ :return:
826
+ :class:`Actor`
827
+ """
828
+ m = cls.name_email_regex.search(string)
829
+ if m:
830
+ name, email = m.groups()
831
+ return Actor(name, email)
832
+ else:
833
+ m = cls.name_only_regex.search(string)
834
+ if m:
835
+ return Actor(m.group(1), None)
836
+ # Assume the best and use the whole string as name.
837
+ return Actor(string, None)
838
+ # END special case name
839
+ # END handle name/email matching
840
+
841
+ @classmethod
842
+ def _main_actor(
843
+ cls,
844
+ env_name: str,
845
+ env_email: str,
846
+ config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None,
847
+ ) -> "Actor":
848
+ actor = Actor("", "")
849
+ user_id = None # We use this to avoid multiple calls to getpass.getuser().
850
+
851
+ def default_email() -> str:
852
+ nonlocal user_id
853
+ if not user_id:
854
+ user_id = get_user_id()
855
+ return user_id
856
+
857
+ def default_name() -> str:
858
+ return default_email().split("@")[0]
859
+
860
+ for attr, evar, cvar, default in (
861
+ ("name", env_name, cls.conf_name, default_name),
862
+ ("email", env_email, cls.conf_email, default_email),
863
+ ):
864
+ try:
865
+ val = os.environ[evar]
866
+ setattr(actor, attr, val)
867
+ except KeyError:
868
+ if config_reader is not None:
869
+ try:
870
+ val = config_reader.get("user", cvar)
871
+ except Exception:
872
+ val = default()
873
+ setattr(actor, attr, val)
874
+ # END config-reader handling
875
+ if not getattr(actor, attr):
876
+ setattr(actor, attr, default())
877
+ # END handle name
878
+ # END for each item to retrieve
879
+ return actor
880
+
881
+ @classmethod
882
+ def committer(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor":
883
+ """
884
+ :return:
885
+ :class:`Actor` instance corresponding to the configured committer. It
886
+ behaves similar to the git implementation, such that the environment will
887
+ override configuration values of `config_reader`. If no value is set at all,
888
+ it will be generated.
889
+
890
+ :param config_reader:
891
+ ConfigReader to use to retrieve the values from in case they are not set in
892
+ the environment.
893
+ """
894
+ return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader)
895
+
896
+ @classmethod
897
+ def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor":
898
+ """Same as :meth:`committer`, but defines the main author. It may be specified
899
+ in the environment, but defaults to the committer."""
900
+ return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
901
+
902
+
903
+ class Stats:
904
+ """Represents stat information as presented by git at the end of a merge. It is
905
+ created from the output of a diff operation.
906
+
907
+ Example::
908
+
909
+ c = Commit( sha1 )
910
+ s = c.stats
911
+ s.total # full-stat-dict
912
+ s.files # dict( filepath : stat-dict )
913
+
914
+ ``stat-dict``
915
+
916
+ A dictionary with the following keys and values::
917
+
918
+ deletions = number of deleted lines as int
919
+ insertions = number of inserted lines as int
920
+ lines = total number of lines changed as int, or deletions + insertions
921
+ change_type = type of change as str, A|C|D|M|R|T|U|X|B
922
+
923
+ ``full-stat-dict``
924
+
925
+ In addition to the items in the stat-dict, it features additional information::
926
+
927
+ files = number of changed files as int
928
+ """
929
+
930
+ __slots__ = ("total", "files")
931
+
932
+ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]) -> None:
933
+ self.total = total
934
+ self.files = files
935
+
936
+ @classmethod
937
+ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
938
+ """Create a :class:`Stats` object from output retrieved by
939
+ :manpage:`git-diff(1)`.
940
+
941
+ :return:
942
+ :class:`git.Stats`
943
+ """
944
+
945
+ hsh: HSH_TD = {
946
+ "total": {"insertions": 0, "deletions": 0, "lines": 0, "files": 0},
947
+ "files": {},
948
+ }
949
+ for line in text.splitlines():
950
+ (change_type, raw_insertions, raw_deletions, filename) = line.split("\t")
951
+ insertions = raw_insertions != "-" and int(raw_insertions) or 0
952
+ deletions = raw_deletions != "-" and int(raw_deletions) or 0
953
+ hsh["total"]["insertions"] += insertions
954
+ hsh["total"]["deletions"] += deletions
955
+ hsh["total"]["lines"] += insertions + deletions
956
+ hsh["total"]["files"] += 1
957
+ files_dict: Files_TD = {
958
+ "insertions": insertions,
959
+ "deletions": deletions,
960
+ "lines": insertions + deletions,
961
+ "change_type": change_type,
962
+ }
963
+ hsh["files"][filename.strip()] = files_dict
964
+ return Stats(hsh["total"], hsh["files"])
965
+
966
+
967
+ class IndexFileSHA1Writer:
968
+ """Wrapper around a file-like object that remembers the SHA1 of the data written to
969
+ it. It will write a sha when the stream is closed or if asked for explicitly using
970
+ :meth:`write_sha`.
971
+
972
+ Only useful to the index file.
973
+
974
+ :note:
975
+ Based on the dulwich project.
976
+ """
977
+
978
+ __slots__ = ("f", "sha1")
979
+
980
+ def __init__(self, f: IO) -> None:
981
+ self.f = f
982
+ self.sha1 = make_sha(b"")
983
+
984
+ def write(self, data: AnyStr) -> int:
985
+ self.sha1.update(data)
986
+ return self.f.write(data)
987
+
988
+ def write_sha(self) -> bytes:
989
+ sha = self.sha1.digest()
990
+ self.f.write(sha)
991
+ return sha
992
+
993
+ def close(self) -> bytes:
994
+ sha = self.write_sha()
995
+ self.f.close()
996
+ return sha
997
+
998
+ def tell(self) -> int:
999
+ return self.f.tell()
1000
+
1001
+
1002
+ class LockFile:
1003
+ """Provides methods to obtain, check for, and release a file based lock which
1004
+ should be used to handle concurrent access to the same file.
1005
+
1006
+ As we are a utility class to be derived from, we only use protected methods.
1007
+
1008
+ Locks will automatically be released on destruction.
1009
+ """
1010
+
1011
+ __slots__ = ("_file_path", "_owns_lock")
1012
+
1013
+ def __init__(self, file_path: PathLike) -> None:
1014
+ self._file_path = file_path
1015
+ self._owns_lock = False
1016
+
1017
+ def __del__(self) -> None:
1018
+ self._release_lock()
1019
+
1020
+ def _lock_file_path(self) -> str:
1021
+ """:return: Path to lockfile"""
1022
+ return "%s.lock" % (self._file_path)
1023
+
1024
+ def _has_lock(self) -> bool:
1025
+ """
1026
+ :return:
1027
+ True if we have a lock and if the lockfile still exists
1028
+
1029
+ :raise AssertionError:
1030
+ If our lock-file does not exist.
1031
+ """
1032
+ return self._owns_lock
1033
+
1034
+ def _obtain_lock_or_raise(self) -> None:
1035
+ """Create a lock file as flag for other instances, mark our instance as
1036
+ lock-holder.
1037
+
1038
+ :raise IOError:
1039
+ If a lock was already present or a lock file could not be written.
1040
+ """
1041
+ if self._has_lock():
1042
+ return
1043
+ lock_file = self._lock_file_path()
1044
+ if osp.isfile(lock_file):
1045
+ raise IOError(
1046
+ "Lock for file %r did already exist, delete %r in case the lock is illegal"
1047
+ % (self._file_path, lock_file)
1048
+ )
1049
+
1050
+ try:
1051
+ with open(lock_file, mode="w"):
1052
+ pass
1053
+ except OSError as e:
1054
+ raise IOError(str(e)) from e
1055
+
1056
+ self._owns_lock = True
1057
+
1058
+ def _obtain_lock(self) -> None:
1059
+ """The default implementation will raise if a lock cannot be obtained.
1060
+
1061
+ Subclasses may override this method to provide a different implementation.
1062
+ """
1063
+ return self._obtain_lock_or_raise()
1064
+
1065
+ def _release_lock(self) -> None:
1066
+ """Release our lock if we have one."""
1067
+ if not self._has_lock():
1068
+ return
1069
+
1070
+ # If someone removed our file beforehand, lets just flag this issue instead of
1071
+ # failing, to make it more usable.
1072
+ lfp = self._lock_file_path()
1073
+ try:
1074
+ rmfile(lfp)
1075
+ except OSError:
1076
+ pass
1077
+ self._owns_lock = False
1078
+
1079
+
1080
+ class BlockingLockFile(LockFile):
1081
+ """The lock file will block until a lock could be obtained, or fail after a
1082
+ specified timeout.
1083
+
1084
+ :note:
1085
+ If the directory containing the lock was removed, an exception will be raised
1086
+ during the blocking period, preventing hangs as the lock can never be obtained.
1087
+ """
1088
+
1089
+ __slots__ = ("_check_interval", "_max_block_time")
1090
+
1091
+ def __init__(
1092
+ self,
1093
+ file_path: PathLike,
1094
+ check_interval_s: float = 0.3,
1095
+ max_block_time_s: int = sys.maxsize,
1096
+ ) -> None:
1097
+ """Configure the instance.
1098
+
1099
+ :param check_interval_s:
1100
+ Period of time to sleep until the lock is checked the next time.
1101
+ By default, it waits a nearly unlimited time.
1102
+
1103
+ :param max_block_time_s:
1104
+ Maximum amount of seconds we may lock.
1105
+ """
1106
+ super().__init__(file_path)
1107
+ self._check_interval = check_interval_s
1108
+ self._max_block_time = max_block_time_s
1109
+
1110
+ def _obtain_lock(self) -> None:
1111
+ """This method blocks until it obtained the lock, or raises :exc:`IOError` if it
1112
+ ran out of time or if the parent directory was not available anymore.
1113
+
1114
+ If this method returns, you are guaranteed to own the lock.
1115
+ """
1116
+ starttime = time.time()
1117
+ maxtime = starttime + float(self._max_block_time)
1118
+ while True:
1119
+ try:
1120
+ super()._obtain_lock()
1121
+ except IOError as e:
1122
+ # synity check: if the directory leading to the lockfile is not
1123
+ # readable anymore, raise an exception
1124
+ curtime = time.time()
1125
+ if not osp.isdir(osp.dirname(self._lock_file_path())):
1126
+ msg = "Directory containing the lockfile %r was not readable anymore after waiting %g seconds" % (
1127
+ self._lock_file_path(),
1128
+ curtime - starttime,
1129
+ )
1130
+ raise IOError(msg) from e
1131
+ # END handle missing directory
1132
+
1133
+ if curtime >= maxtime:
1134
+ msg = "Waited %g seconds for lock at %r" % (
1135
+ maxtime - starttime,
1136
+ self._lock_file_path(),
1137
+ )
1138
+ raise IOError(msg) from e
1139
+ # END abort if we wait too long
1140
+ time.sleep(self._check_interval)
1141
+ else:
1142
+ break
1143
+ # END endless loop
1144
+
1145
+
1146
+ class IterableList(List[T_IterableObj]):
1147
+ """List of iterable objects allowing to query an object by id or by named index::
1148
+
1149
+ heads = repo.heads
1150
+ heads.master
1151
+ heads['master']
1152
+ heads[0]
1153
+
1154
+ Iterable parent objects:
1155
+
1156
+ * :class:`Commit <git.objects.Commit>`
1157
+ * :class:`Submodule <git.objects.submodule.base.Submodule>`
1158
+ * :class:`Reference <git.refs.reference.Reference>`
1159
+ * :class:`FetchInfo <git.remote.FetchInfo>`
1160
+ * :class:`PushInfo <git.remote.PushInfo>`
1161
+
1162
+ Iterable via inheritance:
1163
+
1164
+ * :class:`Head <git.refs.head.Head>`
1165
+ * :class:`TagReference <git.refs.tag.TagReference>`
1166
+ * :class:`RemoteReference <git.refs.remote.RemoteReference>`
1167
+
1168
+ This requires an ``id_attribute`` name to be set which will be queried from its
1169
+ contained items to have a means for comparison.
1170
+
1171
+ A prefix can be specified which is to be used in case the id returned by the items
1172
+ always contains a prefix that does not matter to the user, so it can be left out.
1173
+ """
1174
+
1175
+ __slots__ = ("_id_attr", "_prefix")
1176
+
1177
+ def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[T_IterableObj]":
1178
+ return super().__new__(cls)
1179
+
1180
+ def __init__(self, id_attr: str, prefix: str = "") -> None:
1181
+ self._id_attr = id_attr
1182
+ self._prefix = prefix
1183
+
1184
+ def __contains__(self, attr: object) -> bool:
1185
+ # First try identity match for performance.
1186
+ try:
1187
+ rval = list.__contains__(self, attr)
1188
+ if rval:
1189
+ return rval
1190
+ except (AttributeError, TypeError):
1191
+ pass
1192
+ # END handle match
1193
+
1194
+ # Otherwise make a full name search.
1195
+ try:
1196
+ getattr(self, cast(str, attr)) # Use cast to silence mypy.
1197
+ return True
1198
+ except (AttributeError, TypeError):
1199
+ return False
1200
+ # END handle membership
1201
+
1202
+ def __getattr__(self, attr: str) -> T_IterableObj:
1203
+ attr = self._prefix + attr
1204
+ for item in self:
1205
+ if getattr(item, self._id_attr) == attr:
1206
+ return item
1207
+ # END for each item
1208
+ return list.__getattribute__(self, attr)
1209
+
1210
+ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore[override]
1211
+ if isinstance(index, int):
1212
+ return list.__getitem__(self, index)
1213
+ elif isinstance(index, slice):
1214
+ raise ValueError("Index should be an int or str")
1215
+ else:
1216
+ try:
1217
+ return getattr(self, index)
1218
+ except AttributeError as e:
1219
+ raise IndexError("No item found with id %r" % (self._prefix + index)) from e
1220
+ # END handle getattr
1221
+
1222
+ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None:
1223
+ delindex = cast(int, index)
1224
+ if not isinstance(index, int):
1225
+ delindex = -1
1226
+ name = self._prefix + index
1227
+ for i, item in enumerate(self):
1228
+ if getattr(item, self._id_attr) == name:
1229
+ delindex = i
1230
+ break
1231
+ # END search index
1232
+ # END for each item
1233
+ if delindex == -1:
1234
+ raise IndexError("Item with name %s not found" % name)
1235
+ # END handle error
1236
+ # END get index to delete
1237
+ list.__delitem__(self, delindex)
1238
+
1239
+
1240
+ @runtime_checkable
1241
+ class IterableObj(Protocol):
1242
+ """Defines an interface for iterable items, so there is a uniform way to retrieve
1243
+ and iterate items within the git repository.
1244
+
1245
+ Subclasses:
1246
+
1247
+ * :class:`Submodule <git.objects.submodule.base.Submodule>`
1248
+ * :class:`Commit <git.objects.Commit>`
1249
+ * :class:`Reference <git.refs.reference.Reference>`
1250
+ * :class:`PushInfo <git.remote.PushInfo>`
1251
+ * :class:`FetchInfo <git.remote.FetchInfo>`
1252
+ * :class:`Remote <git.remote.Remote>`
1253
+ """
1254
+
1255
+ __slots__ = ()
1256
+
1257
+ _id_attribute_: str
1258
+
1259
+ @classmethod
1260
+ @abstractmethod
1261
+ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:
1262
+ # Return-typed to be compatible with subtypes e.g. Remote.
1263
+ """Find (all) items of this type.
1264
+
1265
+ Subclasses can specify `args` and `kwargs` differently, and may use them for
1266
+ filtering. However, when the method is called with no additional positional or
1267
+ keyword arguments, subclasses are obliged to to yield all items.
1268
+
1269
+ :return:
1270
+ Iterator yielding Items
1271
+ """
1272
+ raise NotImplementedError("To be implemented by Subclass")
1273
+
1274
+ @classmethod
1275
+ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
1276
+ """Find (all) items of this type and collect them into a list.
1277
+
1278
+ For more information about the arguments, see :meth:`iter_items`.
1279
+
1280
+ :note:
1281
+ Favor the :meth:`iter_items` method as it will avoid eagerly collecting all
1282
+ items. When there are many items, that can slow performance and increase
1283
+ memory usage.
1284
+
1285
+ :return:
1286
+ list(Item,...) list of item instances
1287
+ """
1288
+ out_list: IterableList = IterableList(cls._id_attribute_)
1289
+ out_list.extend(cls.iter_items(repo, *args, **kwargs))
1290
+ return out_list
1291
+
1292
+
1293
+ class IterableClassWatcher(type):
1294
+ """Metaclass that issues :exc:`DeprecationWarning` when :class:`git.util.Iterable`
1295
+ is subclassed."""
1296
+
1297
+ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
1298
+ for base in bases:
1299
+ if type(base) is IterableClassWatcher:
1300
+ warnings.warn(
1301
+ f"GitPython Iterable subclassed by {name}."
1302
+ " Iterable is deprecated due to naming clash since v3.1.18"
1303
+ " and will be removed in 4.0.0."
1304
+ " Use IterableObj instead.",
1305
+ DeprecationWarning,
1306
+ stacklevel=2,
1307
+ )
1308
+
1309
+
1310
+ class Iterable(metaclass=IterableClassWatcher):
1311
+ """Deprecated, use :class:`IterableObj` instead.
1312
+
1313
+ Defines an interface for iterable items, so there is a uniform way to retrieve
1314
+ and iterate items within the git repository.
1315
+ """
1316
+
1317
+ __slots__ = ()
1318
+
1319
+ _id_attribute_ = "attribute that most suitably identifies your instance"
1320
+
1321
+ @classmethod
1322
+ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
1323
+ """Deprecated, use :class:`IterableObj` instead.
1324
+
1325
+ Find (all) items of this type.
1326
+
1327
+ See :meth:`IterableObj.iter_items` for details on usage.
1328
+
1329
+ :return:
1330
+ Iterator yielding Items
1331
+ """
1332
+ raise NotImplementedError("To be implemented by Subclass")
1333
+
1334
+ @classmethod
1335
+ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
1336
+ """Deprecated, use :class:`IterableObj` instead.
1337
+
1338
+ Find (all) items of this type and collect them into a list.
1339
+
1340
+ See :meth:`IterableObj.list_items` for details on usage.
1341
+
1342
+ :return:
1343
+ list(Item,...) list of item instances
1344
+ """
1345
+ out_list: Any = IterableList(cls._id_attribute_)
1346
+ out_list.extend(cls.iter_items(repo, *args, **kwargs))
1347
+ return out_list
1348
+
1349
+
1350
+ # } END classes
.venv/lib/python3.11/site-packages/google/_upb/_message.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fd150616ef20dfeb5709e2ddd83a331b7a02ed5d7f7ab8040e6c05f1276b8a2
3
+ size 401520
.venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/blob.cpython-311.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a36378a9e8c6cca2c6da59dbeef5181249ade2f556ecd8b7f3721f0fed3b09ab
3
+ size 188903
.venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/bucket.cpython-311.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d8412d031a7fba5c4db29cfa7c812d19ef213ef505a76900be69f3933879d52f
3
+ size 159151
.venv/lib/python3.11/site-packages/huggingface_hub/__init__.py ADDED
@@ -0,0 +1,1554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # ***********
16
+ # `huggingface_hub` init has 2 modes:
17
+ # - Normal usage:
18
+ # If imported to use it, all modules and functions are lazy-loaded. This means
19
+ # they exist at top level in module but are imported only the first time they are
20
+ # used. This way, `from huggingface_hub import something` will import `something`
21
+ # quickly without the hassle of importing all the features from `huggingface_hub`.
22
+ # - Static check:
23
+ # If statically analyzed, all modules and functions are loaded normally. This way
24
+ # static typing check works properly as well as autocomplete in text editors and
25
+ # IDEs.
26
+ #
27
+ # The static model imports are done inside the `if TYPE_CHECKING:` statement at
28
+ # the bottom of this file. Since module/functions imports are duplicated, it is
29
+ # mandatory to make sure to add them twice when adding one. This is checked in the
30
+ # `make quality` command.
31
+ #
32
+ # To update the static imports, please run the following command and commit the changes.
33
+ # ```
34
+ # # Use script
35
+ # python utils/check_static_imports.py --update-file
36
+ #
37
+ # # Or run style on codebase
38
+ # make style
39
+ # ```
40
+ #
41
+ # ***********
42
+ # Lazy loader vendored from https://github.com/scientific-python/lazy_loader
43
+ import importlib
44
+ import os
45
+ import sys
46
+ from typing import TYPE_CHECKING
47
+
48
+
49
+ __version__ = "0.36.0"
50
+
51
+ # Alphabetical order of definitions is ensured in tests
52
+ # WARNING: any comment added in this dictionary definition will be lost when
53
+ # re-generating the file !
54
+ _SUBMOD_ATTRS = {
55
+ "_commit_scheduler": [
56
+ "CommitScheduler",
57
+ ],
58
+ "_inference_endpoints": [
59
+ "InferenceEndpoint",
60
+ "InferenceEndpointError",
61
+ "InferenceEndpointStatus",
62
+ "InferenceEndpointTimeoutError",
63
+ "InferenceEndpointType",
64
+ ],
65
+ "_jobs_api": [
66
+ "JobInfo",
67
+ "JobOwner",
68
+ "JobStage",
69
+ "JobStatus",
70
+ ],
71
+ "_login": [
72
+ "auth_list",
73
+ "auth_switch",
74
+ "interpreter_login",
75
+ "login",
76
+ "logout",
77
+ "notebook_login",
78
+ ],
79
+ "_oauth": [
80
+ "OAuthInfo",
81
+ "OAuthOrgInfo",
82
+ "OAuthUserInfo",
83
+ "attach_huggingface_oauth",
84
+ "parse_huggingface_oauth",
85
+ ],
86
+ "_snapshot_download": [
87
+ "snapshot_download",
88
+ ],
89
+ "_space_api": [
90
+ "SpaceHardware",
91
+ "SpaceRuntime",
92
+ "SpaceStage",
93
+ "SpaceStorage",
94
+ "SpaceVariable",
95
+ ],
96
+ "_tensorboard_logger": [
97
+ "HFSummaryWriter",
98
+ ],
99
+ "_webhooks_payload": [
100
+ "WebhookPayload",
101
+ "WebhookPayloadComment",
102
+ "WebhookPayloadDiscussion",
103
+ "WebhookPayloadDiscussionChanges",
104
+ "WebhookPayloadEvent",
105
+ "WebhookPayloadMovedTo",
106
+ "WebhookPayloadRepo",
107
+ "WebhookPayloadUrl",
108
+ "WebhookPayloadWebhook",
109
+ ],
110
+ "_webhooks_server": [
111
+ "WebhooksServer",
112
+ "webhook_endpoint",
113
+ ],
114
+ "community": [
115
+ "Discussion",
116
+ "DiscussionComment",
117
+ "DiscussionCommit",
118
+ "DiscussionEvent",
119
+ "DiscussionStatusChange",
120
+ "DiscussionTitleChange",
121
+ "DiscussionWithDetails",
122
+ ],
123
+ "constants": [
124
+ "CONFIG_NAME",
125
+ "FLAX_WEIGHTS_NAME",
126
+ "HUGGINGFACE_CO_URL_HOME",
127
+ "HUGGINGFACE_CO_URL_TEMPLATE",
128
+ "PYTORCH_WEIGHTS_NAME",
129
+ "REPO_TYPE_DATASET",
130
+ "REPO_TYPE_MODEL",
131
+ "REPO_TYPE_SPACE",
132
+ "TF2_WEIGHTS_NAME",
133
+ "TF_WEIGHTS_NAME",
134
+ ],
135
+ "fastai_utils": [
136
+ "_save_pretrained_fastai",
137
+ "from_pretrained_fastai",
138
+ "push_to_hub_fastai",
139
+ ],
140
+ "file_download": [
141
+ "HfFileMetadata",
142
+ "_CACHED_NO_EXIST",
143
+ "get_hf_file_metadata",
144
+ "hf_hub_download",
145
+ "hf_hub_url",
146
+ "try_to_load_from_cache",
147
+ ],
148
+ "hf_api": [
149
+ "Collection",
150
+ "CollectionItem",
151
+ "CommitInfo",
152
+ "CommitOperation",
153
+ "CommitOperationAdd",
154
+ "CommitOperationCopy",
155
+ "CommitOperationDelete",
156
+ "DatasetInfo",
157
+ "GitCommitInfo",
158
+ "GitRefInfo",
159
+ "GitRefs",
160
+ "HfApi",
161
+ "ModelInfo",
162
+ "Organization",
163
+ "RepoUrl",
164
+ "SpaceInfo",
165
+ "User",
166
+ "UserLikes",
167
+ "WebhookInfo",
168
+ "WebhookWatchedItem",
169
+ "accept_access_request",
170
+ "add_collection_item",
171
+ "add_space_secret",
172
+ "add_space_variable",
173
+ "auth_check",
174
+ "cancel_access_request",
175
+ "cancel_job",
176
+ "change_discussion_status",
177
+ "comment_discussion",
178
+ "create_branch",
179
+ "create_collection",
180
+ "create_commit",
181
+ "create_discussion",
182
+ "create_inference_endpoint",
183
+ "create_inference_endpoint_from_catalog",
184
+ "create_pull_request",
185
+ "create_repo",
186
+ "create_scheduled_job",
187
+ "create_scheduled_uv_job",
188
+ "create_tag",
189
+ "create_webhook",
190
+ "dataset_info",
191
+ "delete_branch",
192
+ "delete_collection",
193
+ "delete_collection_item",
194
+ "delete_file",
195
+ "delete_folder",
196
+ "delete_inference_endpoint",
197
+ "delete_repo",
198
+ "delete_scheduled_job",
199
+ "delete_space_secret",
200
+ "delete_space_storage",
201
+ "delete_space_variable",
202
+ "delete_tag",
203
+ "delete_webhook",
204
+ "disable_webhook",
205
+ "duplicate_space",
206
+ "edit_discussion_comment",
207
+ "enable_webhook",
208
+ "fetch_job_logs",
209
+ "file_exists",
210
+ "get_collection",
211
+ "get_dataset_tags",
212
+ "get_discussion_details",
213
+ "get_full_repo_name",
214
+ "get_inference_endpoint",
215
+ "get_model_tags",
216
+ "get_organization_overview",
217
+ "get_paths_info",
218
+ "get_repo_discussions",
219
+ "get_safetensors_metadata",
220
+ "get_space_runtime",
221
+ "get_space_variables",
222
+ "get_token_permission",
223
+ "get_user_overview",
224
+ "get_webhook",
225
+ "grant_access",
226
+ "inspect_job",
227
+ "inspect_scheduled_job",
228
+ "list_accepted_access_requests",
229
+ "list_collections",
230
+ "list_datasets",
231
+ "list_inference_catalog",
232
+ "list_inference_endpoints",
233
+ "list_jobs",
234
+ "list_lfs_files",
235
+ "list_liked_repos",
236
+ "list_models",
237
+ "list_organization_members",
238
+ "list_papers",
239
+ "list_pending_access_requests",
240
+ "list_rejected_access_requests",
241
+ "list_repo_commits",
242
+ "list_repo_files",
243
+ "list_repo_likers",
244
+ "list_repo_refs",
245
+ "list_repo_tree",
246
+ "list_spaces",
247
+ "list_user_followers",
248
+ "list_user_following",
249
+ "list_webhooks",
250
+ "merge_pull_request",
251
+ "model_info",
252
+ "move_repo",
253
+ "paper_info",
254
+ "parse_safetensors_file_metadata",
255
+ "pause_inference_endpoint",
256
+ "pause_space",
257
+ "permanently_delete_lfs_files",
258
+ "preupload_lfs_files",
259
+ "reject_access_request",
260
+ "rename_discussion",
261
+ "repo_exists",
262
+ "repo_info",
263
+ "repo_type_and_id_from_hf_id",
264
+ "request_space_hardware",
265
+ "request_space_storage",
266
+ "restart_space",
267
+ "resume_inference_endpoint",
268
+ "resume_scheduled_job",
269
+ "revision_exists",
270
+ "run_as_future",
271
+ "run_job",
272
+ "run_uv_job",
273
+ "scale_to_zero_inference_endpoint",
274
+ "set_space_sleep_time",
275
+ "space_info",
276
+ "super_squash_history",
277
+ "suspend_scheduled_job",
278
+ "unlike",
279
+ "update_collection_item",
280
+ "update_collection_metadata",
281
+ "update_inference_endpoint",
282
+ "update_repo_settings",
283
+ "update_repo_visibility",
284
+ "update_webhook",
285
+ "upload_file",
286
+ "upload_folder",
287
+ "upload_large_folder",
288
+ "whoami",
289
+ ],
290
+ "hf_file_system": [
291
+ "HfFileSystem",
292
+ "HfFileSystemFile",
293
+ "HfFileSystemResolvedPath",
294
+ "HfFileSystemStreamFile",
295
+ ],
296
+ "hub_mixin": [
297
+ "ModelHubMixin",
298
+ "PyTorchModelHubMixin",
299
+ ],
300
+ "inference._client": [
301
+ "InferenceClient",
302
+ "InferenceTimeoutError",
303
+ ],
304
+ "inference._generated._async_client": [
305
+ "AsyncInferenceClient",
306
+ ],
307
+ "inference._generated.types": [
308
+ "AudioClassificationInput",
309
+ "AudioClassificationOutputElement",
310
+ "AudioClassificationOutputTransform",
311
+ "AudioClassificationParameters",
312
+ "AudioToAudioInput",
313
+ "AudioToAudioOutputElement",
314
+ "AutomaticSpeechRecognitionEarlyStoppingEnum",
315
+ "AutomaticSpeechRecognitionGenerationParameters",
316
+ "AutomaticSpeechRecognitionInput",
317
+ "AutomaticSpeechRecognitionOutput",
318
+ "AutomaticSpeechRecognitionOutputChunk",
319
+ "AutomaticSpeechRecognitionParameters",
320
+ "ChatCompletionInput",
321
+ "ChatCompletionInputFunctionDefinition",
322
+ "ChatCompletionInputFunctionName",
323
+ "ChatCompletionInputGrammarType",
324
+ "ChatCompletionInputJSONSchema",
325
+ "ChatCompletionInputMessage",
326
+ "ChatCompletionInputMessageChunk",
327
+ "ChatCompletionInputMessageChunkType",
328
+ "ChatCompletionInputResponseFormatJSONObject",
329
+ "ChatCompletionInputResponseFormatJSONSchema",
330
+ "ChatCompletionInputResponseFormatText",
331
+ "ChatCompletionInputStreamOptions",
332
+ "ChatCompletionInputTool",
333
+ "ChatCompletionInputToolCall",
334
+ "ChatCompletionInputToolChoiceClass",
335
+ "ChatCompletionInputToolChoiceEnum",
336
+ "ChatCompletionInputURL",
337
+ "ChatCompletionOutput",
338
+ "ChatCompletionOutputComplete",
339
+ "ChatCompletionOutputFunctionDefinition",
340
+ "ChatCompletionOutputLogprob",
341
+ "ChatCompletionOutputLogprobs",
342
+ "ChatCompletionOutputMessage",
343
+ "ChatCompletionOutputToolCall",
344
+ "ChatCompletionOutputTopLogprob",
345
+ "ChatCompletionOutputUsage",
346
+ "ChatCompletionStreamOutput",
347
+ "ChatCompletionStreamOutputChoice",
348
+ "ChatCompletionStreamOutputDelta",
349
+ "ChatCompletionStreamOutputDeltaToolCall",
350
+ "ChatCompletionStreamOutputFunction",
351
+ "ChatCompletionStreamOutputLogprob",
352
+ "ChatCompletionStreamOutputLogprobs",
353
+ "ChatCompletionStreamOutputTopLogprob",
354
+ "ChatCompletionStreamOutputUsage",
355
+ "DepthEstimationInput",
356
+ "DepthEstimationOutput",
357
+ "DocumentQuestionAnsweringInput",
358
+ "DocumentQuestionAnsweringInputData",
359
+ "DocumentQuestionAnsweringOutputElement",
360
+ "DocumentQuestionAnsweringParameters",
361
+ "FeatureExtractionInput",
362
+ "FeatureExtractionInputTruncationDirection",
363
+ "FillMaskInput",
364
+ "FillMaskOutputElement",
365
+ "FillMaskParameters",
366
+ "ImageClassificationInput",
367
+ "ImageClassificationOutputElement",
368
+ "ImageClassificationOutputTransform",
369
+ "ImageClassificationParameters",
370
+ "ImageSegmentationInput",
371
+ "ImageSegmentationOutputElement",
372
+ "ImageSegmentationParameters",
373
+ "ImageSegmentationSubtask",
374
+ "ImageToImageInput",
375
+ "ImageToImageOutput",
376
+ "ImageToImageParameters",
377
+ "ImageToImageTargetSize",
378
+ "ImageToTextEarlyStoppingEnum",
379
+ "ImageToTextGenerationParameters",
380
+ "ImageToTextInput",
381
+ "ImageToTextOutput",
382
+ "ImageToTextParameters",
383
+ "ImageToVideoInput",
384
+ "ImageToVideoOutput",
385
+ "ImageToVideoParameters",
386
+ "ImageToVideoTargetSize",
387
+ "ObjectDetectionBoundingBox",
388
+ "ObjectDetectionInput",
389
+ "ObjectDetectionOutputElement",
390
+ "ObjectDetectionParameters",
391
+ "Padding",
392
+ "QuestionAnsweringInput",
393
+ "QuestionAnsweringInputData",
394
+ "QuestionAnsweringOutputElement",
395
+ "QuestionAnsweringParameters",
396
+ "SentenceSimilarityInput",
397
+ "SentenceSimilarityInputData",
398
+ "SummarizationInput",
399
+ "SummarizationOutput",
400
+ "SummarizationParameters",
401
+ "SummarizationTruncationStrategy",
402
+ "TableQuestionAnsweringInput",
403
+ "TableQuestionAnsweringInputData",
404
+ "TableQuestionAnsweringOutputElement",
405
+ "TableQuestionAnsweringParameters",
406
+ "Text2TextGenerationInput",
407
+ "Text2TextGenerationOutput",
408
+ "Text2TextGenerationParameters",
409
+ "Text2TextGenerationTruncationStrategy",
410
+ "TextClassificationInput",
411
+ "TextClassificationOutputElement",
412
+ "TextClassificationOutputTransform",
413
+ "TextClassificationParameters",
414
+ "TextGenerationInput",
415
+ "TextGenerationInputGenerateParameters",
416
+ "TextGenerationInputGrammarType",
417
+ "TextGenerationOutput",
418
+ "TextGenerationOutputBestOfSequence",
419
+ "TextGenerationOutputDetails",
420
+ "TextGenerationOutputFinishReason",
421
+ "TextGenerationOutputPrefillToken",
422
+ "TextGenerationOutputToken",
423
+ "TextGenerationStreamOutput",
424
+ "TextGenerationStreamOutputStreamDetails",
425
+ "TextGenerationStreamOutputToken",
426
+ "TextToAudioEarlyStoppingEnum",
427
+ "TextToAudioGenerationParameters",
428
+ "TextToAudioInput",
429
+ "TextToAudioOutput",
430
+ "TextToAudioParameters",
431
+ "TextToImageInput",
432
+ "TextToImageOutput",
433
+ "TextToImageParameters",
434
+ "TextToSpeechEarlyStoppingEnum",
435
+ "TextToSpeechGenerationParameters",
436
+ "TextToSpeechInput",
437
+ "TextToSpeechOutput",
438
+ "TextToSpeechParameters",
439
+ "TextToVideoInput",
440
+ "TextToVideoOutput",
441
+ "TextToVideoParameters",
442
+ "TokenClassificationAggregationStrategy",
443
+ "TokenClassificationInput",
444
+ "TokenClassificationOutputElement",
445
+ "TokenClassificationParameters",
446
+ "TranslationInput",
447
+ "TranslationOutput",
448
+ "TranslationParameters",
449
+ "TranslationTruncationStrategy",
450
+ "TypeEnum",
451
+ "VideoClassificationInput",
452
+ "VideoClassificationOutputElement",
453
+ "VideoClassificationOutputTransform",
454
+ "VideoClassificationParameters",
455
+ "VisualQuestionAnsweringInput",
456
+ "VisualQuestionAnsweringInputData",
457
+ "VisualQuestionAnsweringOutputElement",
458
+ "VisualQuestionAnsweringParameters",
459
+ "ZeroShotClassificationInput",
460
+ "ZeroShotClassificationOutputElement",
461
+ "ZeroShotClassificationParameters",
462
+ "ZeroShotImageClassificationInput",
463
+ "ZeroShotImageClassificationOutputElement",
464
+ "ZeroShotImageClassificationParameters",
465
+ "ZeroShotObjectDetectionBoundingBox",
466
+ "ZeroShotObjectDetectionInput",
467
+ "ZeroShotObjectDetectionOutputElement",
468
+ "ZeroShotObjectDetectionParameters",
469
+ ],
470
+ "inference._mcp.agent": [
471
+ "Agent",
472
+ ],
473
+ "inference._mcp.mcp_client": [
474
+ "MCPClient",
475
+ ],
476
+ "inference_api": [
477
+ "InferenceApi",
478
+ ],
479
+ "keras_mixin": [
480
+ "KerasModelHubMixin",
481
+ "from_pretrained_keras",
482
+ "push_to_hub_keras",
483
+ "save_pretrained_keras",
484
+ ],
485
+ "repocard": [
486
+ "DatasetCard",
487
+ "ModelCard",
488
+ "RepoCard",
489
+ "SpaceCard",
490
+ "metadata_eval_result",
491
+ "metadata_load",
492
+ "metadata_save",
493
+ "metadata_update",
494
+ ],
495
+ "repocard_data": [
496
+ "CardData",
497
+ "DatasetCardData",
498
+ "EvalResult",
499
+ "ModelCardData",
500
+ "SpaceCardData",
501
+ ],
502
+ "repository": [
503
+ "Repository",
504
+ ],
505
+ "serialization": [
506
+ "StateDictSplit",
507
+ "get_tf_storage_size",
508
+ "get_torch_storage_id",
509
+ "get_torch_storage_size",
510
+ "load_state_dict_from_file",
511
+ "load_torch_model",
512
+ "save_torch_model",
513
+ "save_torch_state_dict",
514
+ "split_state_dict_into_shards_factory",
515
+ "split_tf_state_dict_into_shards",
516
+ "split_torch_state_dict_into_shards",
517
+ ],
518
+ "serialization._dduf": [
519
+ "DDUFEntry",
520
+ "export_entries_as_dduf",
521
+ "export_folder_as_dduf",
522
+ "read_dduf_file",
523
+ ],
524
+ "utils": [
525
+ "CacheNotFound",
526
+ "CachedFileInfo",
527
+ "CachedRepoInfo",
528
+ "CachedRevisionInfo",
529
+ "CorruptedCacheException",
530
+ "DeleteCacheStrategy",
531
+ "HFCacheInfo",
532
+ "HfFolder",
533
+ "cached_assets_path",
534
+ "configure_http_backend",
535
+ "dump_environment_info",
536
+ "get_session",
537
+ "get_token",
538
+ "logging",
539
+ "scan_cache_dir",
540
+ ],
541
+ }
542
+
543
+ # WARNING: __all__ is generated automatically, Any manual edit will be lost when re-generating this file !
544
+ #
545
+ # To update the static imports, please run the following command and commit the changes.
546
+ # ```
547
+ # # Use script
548
+ # python utils/check_all_variable.py --update
549
+ #
550
+ # # Or run style on codebase
551
+ # make style
552
+ # ```
553
+
554
+ __all__ = [
555
+ "Agent",
556
+ "AsyncInferenceClient",
557
+ "AudioClassificationInput",
558
+ "AudioClassificationOutputElement",
559
+ "AudioClassificationOutputTransform",
560
+ "AudioClassificationParameters",
561
+ "AudioToAudioInput",
562
+ "AudioToAudioOutputElement",
563
+ "AutomaticSpeechRecognitionEarlyStoppingEnum",
564
+ "AutomaticSpeechRecognitionGenerationParameters",
565
+ "AutomaticSpeechRecognitionInput",
566
+ "AutomaticSpeechRecognitionOutput",
567
+ "AutomaticSpeechRecognitionOutputChunk",
568
+ "AutomaticSpeechRecognitionParameters",
569
+ "CONFIG_NAME",
570
+ "CacheNotFound",
571
+ "CachedFileInfo",
572
+ "CachedRepoInfo",
573
+ "CachedRevisionInfo",
574
+ "CardData",
575
+ "ChatCompletionInput",
576
+ "ChatCompletionInputFunctionDefinition",
577
+ "ChatCompletionInputFunctionName",
578
+ "ChatCompletionInputGrammarType",
579
+ "ChatCompletionInputJSONSchema",
580
+ "ChatCompletionInputMessage",
581
+ "ChatCompletionInputMessageChunk",
582
+ "ChatCompletionInputMessageChunkType",
583
+ "ChatCompletionInputResponseFormatJSONObject",
584
+ "ChatCompletionInputResponseFormatJSONSchema",
585
+ "ChatCompletionInputResponseFormatText",
586
+ "ChatCompletionInputStreamOptions",
587
+ "ChatCompletionInputTool",
588
+ "ChatCompletionInputToolCall",
589
+ "ChatCompletionInputToolChoiceClass",
590
+ "ChatCompletionInputToolChoiceEnum",
591
+ "ChatCompletionInputURL",
592
+ "ChatCompletionOutput",
593
+ "ChatCompletionOutputComplete",
594
+ "ChatCompletionOutputFunctionDefinition",
595
+ "ChatCompletionOutputLogprob",
596
+ "ChatCompletionOutputLogprobs",
597
+ "ChatCompletionOutputMessage",
598
+ "ChatCompletionOutputToolCall",
599
+ "ChatCompletionOutputTopLogprob",
600
+ "ChatCompletionOutputUsage",
601
+ "ChatCompletionStreamOutput",
602
+ "ChatCompletionStreamOutputChoice",
603
+ "ChatCompletionStreamOutputDelta",
604
+ "ChatCompletionStreamOutputDeltaToolCall",
605
+ "ChatCompletionStreamOutputFunction",
606
+ "ChatCompletionStreamOutputLogprob",
607
+ "ChatCompletionStreamOutputLogprobs",
608
+ "ChatCompletionStreamOutputTopLogprob",
609
+ "ChatCompletionStreamOutputUsage",
610
+ "Collection",
611
+ "CollectionItem",
612
+ "CommitInfo",
613
+ "CommitOperation",
614
+ "CommitOperationAdd",
615
+ "CommitOperationCopy",
616
+ "CommitOperationDelete",
617
+ "CommitScheduler",
618
+ "CorruptedCacheException",
619
+ "DDUFEntry",
620
+ "DatasetCard",
621
+ "DatasetCardData",
622
+ "DatasetInfo",
623
+ "DeleteCacheStrategy",
624
+ "DepthEstimationInput",
625
+ "DepthEstimationOutput",
626
+ "Discussion",
627
+ "DiscussionComment",
628
+ "DiscussionCommit",
629
+ "DiscussionEvent",
630
+ "DiscussionStatusChange",
631
+ "DiscussionTitleChange",
632
+ "DiscussionWithDetails",
633
+ "DocumentQuestionAnsweringInput",
634
+ "DocumentQuestionAnsweringInputData",
635
+ "DocumentQuestionAnsweringOutputElement",
636
+ "DocumentQuestionAnsweringParameters",
637
+ "EvalResult",
638
+ "FLAX_WEIGHTS_NAME",
639
+ "FeatureExtractionInput",
640
+ "FeatureExtractionInputTruncationDirection",
641
+ "FillMaskInput",
642
+ "FillMaskOutputElement",
643
+ "FillMaskParameters",
644
+ "GitCommitInfo",
645
+ "GitRefInfo",
646
+ "GitRefs",
647
+ "HFCacheInfo",
648
+ "HFSummaryWriter",
649
+ "HUGGINGFACE_CO_URL_HOME",
650
+ "HUGGINGFACE_CO_URL_TEMPLATE",
651
+ "HfApi",
652
+ "HfFileMetadata",
653
+ "HfFileSystem",
654
+ "HfFileSystemFile",
655
+ "HfFileSystemResolvedPath",
656
+ "HfFileSystemStreamFile",
657
+ "HfFolder",
658
+ "ImageClassificationInput",
659
+ "ImageClassificationOutputElement",
660
+ "ImageClassificationOutputTransform",
661
+ "ImageClassificationParameters",
662
+ "ImageSegmentationInput",
663
+ "ImageSegmentationOutputElement",
664
+ "ImageSegmentationParameters",
665
+ "ImageSegmentationSubtask",
666
+ "ImageToImageInput",
667
+ "ImageToImageOutput",
668
+ "ImageToImageParameters",
669
+ "ImageToImageTargetSize",
670
+ "ImageToTextEarlyStoppingEnum",
671
+ "ImageToTextGenerationParameters",
672
+ "ImageToTextInput",
673
+ "ImageToTextOutput",
674
+ "ImageToTextParameters",
675
+ "ImageToVideoInput",
676
+ "ImageToVideoOutput",
677
+ "ImageToVideoParameters",
678
+ "ImageToVideoTargetSize",
679
+ "InferenceApi",
680
+ "InferenceClient",
681
+ "InferenceEndpoint",
682
+ "InferenceEndpointError",
683
+ "InferenceEndpointStatus",
684
+ "InferenceEndpointTimeoutError",
685
+ "InferenceEndpointType",
686
+ "InferenceTimeoutError",
687
+ "JobInfo",
688
+ "JobOwner",
689
+ "JobStage",
690
+ "JobStatus",
691
+ "KerasModelHubMixin",
692
+ "MCPClient",
693
+ "ModelCard",
694
+ "ModelCardData",
695
+ "ModelHubMixin",
696
+ "ModelInfo",
697
+ "OAuthInfo",
698
+ "OAuthOrgInfo",
699
+ "OAuthUserInfo",
700
+ "ObjectDetectionBoundingBox",
701
+ "ObjectDetectionInput",
702
+ "ObjectDetectionOutputElement",
703
+ "ObjectDetectionParameters",
704
+ "Organization",
705
+ "PYTORCH_WEIGHTS_NAME",
706
+ "Padding",
707
+ "PyTorchModelHubMixin",
708
+ "QuestionAnsweringInput",
709
+ "QuestionAnsweringInputData",
710
+ "QuestionAnsweringOutputElement",
711
+ "QuestionAnsweringParameters",
712
+ "REPO_TYPE_DATASET",
713
+ "REPO_TYPE_MODEL",
714
+ "REPO_TYPE_SPACE",
715
+ "RepoCard",
716
+ "RepoUrl",
717
+ "Repository",
718
+ "SentenceSimilarityInput",
719
+ "SentenceSimilarityInputData",
720
+ "SpaceCard",
721
+ "SpaceCardData",
722
+ "SpaceHardware",
723
+ "SpaceInfo",
724
+ "SpaceRuntime",
725
+ "SpaceStage",
726
+ "SpaceStorage",
727
+ "SpaceVariable",
728
+ "StateDictSplit",
729
+ "SummarizationInput",
730
+ "SummarizationOutput",
731
+ "SummarizationParameters",
732
+ "SummarizationTruncationStrategy",
733
+ "TF2_WEIGHTS_NAME",
734
+ "TF_WEIGHTS_NAME",
735
+ "TableQuestionAnsweringInput",
736
+ "TableQuestionAnsweringInputData",
737
+ "TableQuestionAnsweringOutputElement",
738
+ "TableQuestionAnsweringParameters",
739
+ "Text2TextGenerationInput",
740
+ "Text2TextGenerationOutput",
741
+ "Text2TextGenerationParameters",
742
+ "Text2TextGenerationTruncationStrategy",
743
+ "TextClassificationInput",
744
+ "TextClassificationOutputElement",
745
+ "TextClassificationOutputTransform",
746
+ "TextClassificationParameters",
747
+ "TextGenerationInput",
748
+ "TextGenerationInputGenerateParameters",
749
+ "TextGenerationInputGrammarType",
750
+ "TextGenerationOutput",
751
+ "TextGenerationOutputBestOfSequence",
752
+ "TextGenerationOutputDetails",
753
+ "TextGenerationOutputFinishReason",
754
+ "TextGenerationOutputPrefillToken",
755
+ "TextGenerationOutputToken",
756
+ "TextGenerationStreamOutput",
757
+ "TextGenerationStreamOutputStreamDetails",
758
+ "TextGenerationStreamOutputToken",
759
+ "TextToAudioEarlyStoppingEnum",
760
+ "TextToAudioGenerationParameters",
761
+ "TextToAudioInput",
762
+ "TextToAudioOutput",
763
+ "TextToAudioParameters",
764
+ "TextToImageInput",
765
+ "TextToImageOutput",
766
+ "TextToImageParameters",
767
+ "TextToSpeechEarlyStoppingEnum",
768
+ "TextToSpeechGenerationParameters",
769
+ "TextToSpeechInput",
770
+ "TextToSpeechOutput",
771
+ "TextToSpeechParameters",
772
+ "TextToVideoInput",
773
+ "TextToVideoOutput",
774
+ "TextToVideoParameters",
775
+ "TokenClassificationAggregationStrategy",
776
+ "TokenClassificationInput",
777
+ "TokenClassificationOutputElement",
778
+ "TokenClassificationParameters",
779
+ "TranslationInput",
780
+ "TranslationOutput",
781
+ "TranslationParameters",
782
+ "TranslationTruncationStrategy",
783
+ "TypeEnum",
784
+ "User",
785
+ "UserLikes",
786
+ "VideoClassificationInput",
787
+ "VideoClassificationOutputElement",
788
+ "VideoClassificationOutputTransform",
789
+ "VideoClassificationParameters",
790
+ "VisualQuestionAnsweringInput",
791
+ "VisualQuestionAnsweringInputData",
792
+ "VisualQuestionAnsweringOutputElement",
793
+ "VisualQuestionAnsweringParameters",
794
+ "WebhookInfo",
795
+ "WebhookPayload",
796
+ "WebhookPayloadComment",
797
+ "WebhookPayloadDiscussion",
798
+ "WebhookPayloadDiscussionChanges",
799
+ "WebhookPayloadEvent",
800
+ "WebhookPayloadMovedTo",
801
+ "WebhookPayloadRepo",
802
+ "WebhookPayloadUrl",
803
+ "WebhookPayloadWebhook",
804
+ "WebhookWatchedItem",
805
+ "WebhooksServer",
806
+ "ZeroShotClassificationInput",
807
+ "ZeroShotClassificationOutputElement",
808
+ "ZeroShotClassificationParameters",
809
+ "ZeroShotImageClassificationInput",
810
+ "ZeroShotImageClassificationOutputElement",
811
+ "ZeroShotImageClassificationParameters",
812
+ "ZeroShotObjectDetectionBoundingBox",
813
+ "ZeroShotObjectDetectionInput",
814
+ "ZeroShotObjectDetectionOutputElement",
815
+ "ZeroShotObjectDetectionParameters",
816
+ "_CACHED_NO_EXIST",
817
+ "_save_pretrained_fastai",
818
+ "accept_access_request",
819
+ "add_collection_item",
820
+ "add_space_secret",
821
+ "add_space_variable",
822
+ "attach_huggingface_oauth",
823
+ "auth_check",
824
+ "auth_list",
825
+ "auth_switch",
826
+ "cached_assets_path",
827
+ "cancel_access_request",
828
+ "cancel_job",
829
+ "change_discussion_status",
830
+ "comment_discussion",
831
+ "configure_http_backend",
832
+ "create_branch",
833
+ "create_collection",
834
+ "create_commit",
835
+ "create_discussion",
836
+ "create_inference_endpoint",
837
+ "create_inference_endpoint_from_catalog",
838
+ "create_pull_request",
839
+ "create_repo",
840
+ "create_scheduled_job",
841
+ "create_scheduled_uv_job",
842
+ "create_tag",
843
+ "create_webhook",
844
+ "dataset_info",
845
+ "delete_branch",
846
+ "delete_collection",
847
+ "delete_collection_item",
848
+ "delete_file",
849
+ "delete_folder",
850
+ "delete_inference_endpoint",
851
+ "delete_repo",
852
+ "delete_scheduled_job",
853
+ "delete_space_secret",
854
+ "delete_space_storage",
855
+ "delete_space_variable",
856
+ "delete_tag",
857
+ "delete_webhook",
858
+ "disable_webhook",
859
+ "dump_environment_info",
860
+ "duplicate_space",
861
+ "edit_discussion_comment",
862
+ "enable_webhook",
863
+ "export_entries_as_dduf",
864
+ "export_folder_as_dduf",
865
+ "fetch_job_logs",
866
+ "file_exists",
867
+ "from_pretrained_fastai",
868
+ "from_pretrained_keras",
869
+ "get_collection",
870
+ "get_dataset_tags",
871
+ "get_discussion_details",
872
+ "get_full_repo_name",
873
+ "get_hf_file_metadata",
874
+ "get_inference_endpoint",
875
+ "get_model_tags",
876
+ "get_organization_overview",
877
+ "get_paths_info",
878
+ "get_repo_discussions",
879
+ "get_safetensors_metadata",
880
+ "get_session",
881
+ "get_space_runtime",
882
+ "get_space_variables",
883
+ "get_tf_storage_size",
884
+ "get_token",
885
+ "get_token_permission",
886
+ "get_torch_storage_id",
887
+ "get_torch_storage_size",
888
+ "get_user_overview",
889
+ "get_webhook",
890
+ "grant_access",
891
+ "hf_hub_download",
892
+ "hf_hub_url",
893
+ "inspect_job",
894
+ "inspect_scheduled_job",
895
+ "interpreter_login",
896
+ "list_accepted_access_requests",
897
+ "list_collections",
898
+ "list_datasets",
899
+ "list_inference_catalog",
900
+ "list_inference_endpoints",
901
+ "list_jobs",
902
+ "list_lfs_files",
903
+ "list_liked_repos",
904
+ "list_models",
905
+ "list_organization_members",
906
+ "list_papers",
907
+ "list_pending_access_requests",
908
+ "list_rejected_access_requests",
909
+ "list_repo_commits",
910
+ "list_repo_files",
911
+ "list_repo_likers",
912
+ "list_repo_refs",
913
+ "list_repo_tree",
914
+ "list_spaces",
915
+ "list_user_followers",
916
+ "list_user_following",
917
+ "list_webhooks",
918
+ "load_state_dict_from_file",
919
+ "load_torch_model",
920
+ "logging",
921
+ "login",
922
+ "logout",
923
+ "merge_pull_request",
924
+ "metadata_eval_result",
925
+ "metadata_load",
926
+ "metadata_save",
927
+ "metadata_update",
928
+ "model_info",
929
+ "move_repo",
930
+ "notebook_login",
931
+ "paper_info",
932
+ "parse_huggingface_oauth",
933
+ "parse_safetensors_file_metadata",
934
+ "pause_inference_endpoint",
935
+ "pause_space",
936
+ "permanently_delete_lfs_files",
937
+ "preupload_lfs_files",
938
+ "push_to_hub_fastai",
939
+ "push_to_hub_keras",
940
+ "read_dduf_file",
941
+ "reject_access_request",
942
+ "rename_discussion",
943
+ "repo_exists",
944
+ "repo_info",
945
+ "repo_type_and_id_from_hf_id",
946
+ "request_space_hardware",
947
+ "request_space_storage",
948
+ "restart_space",
949
+ "resume_inference_endpoint",
950
+ "resume_scheduled_job",
951
+ "revision_exists",
952
+ "run_as_future",
953
+ "run_job",
954
+ "run_uv_job",
955
+ "save_pretrained_keras",
956
+ "save_torch_model",
957
+ "save_torch_state_dict",
958
+ "scale_to_zero_inference_endpoint",
959
+ "scan_cache_dir",
960
+ "set_space_sleep_time",
961
+ "snapshot_download",
962
+ "space_info",
963
+ "split_state_dict_into_shards_factory",
964
+ "split_tf_state_dict_into_shards",
965
+ "split_torch_state_dict_into_shards",
966
+ "super_squash_history",
967
+ "suspend_scheduled_job",
968
+ "try_to_load_from_cache",
969
+ "unlike",
970
+ "update_collection_item",
971
+ "update_collection_metadata",
972
+ "update_inference_endpoint",
973
+ "update_repo_settings",
974
+ "update_repo_visibility",
975
+ "update_webhook",
976
+ "upload_file",
977
+ "upload_folder",
978
+ "upload_large_folder",
979
+ "webhook_endpoint",
980
+ "whoami",
981
+ ]
982
+
983
+
984
+ def _attach(package_name, submodules=None, submod_attrs=None):
985
+ """Attach lazily loaded submodules, functions, or other attributes.
986
+
987
+ Typically, modules import submodules and attributes as follows:
988
+
989
+ ```py
990
+ import mysubmodule
991
+ import anothersubmodule
992
+
993
+ from .foo import someattr
994
+ ```
995
+
996
+ The idea is to replace a package's `__getattr__`, `__dir__`, such that all imports
997
+ work exactly the way they would with normal imports, except that the import occurs
998
+ upon first use.
999
+
1000
+ The typical way to call this function, replacing the above imports, is:
1001
+
1002
+ ```python
1003
+ __getattr__, __dir__ = lazy.attach(
1004
+ __name__,
1005
+ ['mysubmodule', 'anothersubmodule'],
1006
+ {'foo': ['someattr']}
1007
+ )
1008
+ ```
1009
+ This functionality requires Python 3.7 or higher.
1010
+
1011
+ Args:
1012
+ package_name (`str`):
1013
+ Typically use `__name__`.
1014
+ submodules (`set`):
1015
+ List of submodules to attach.
1016
+ submod_attrs (`dict`):
1017
+ Dictionary of submodule -> list of attributes / functions.
1018
+ These attributes are imported as they are used.
1019
+
1020
+ Returns:
1021
+ __getattr__, __dir__, __all__
1022
+
1023
+ """
1024
+ if submod_attrs is None:
1025
+ submod_attrs = {}
1026
+
1027
+ if submodules is None:
1028
+ submodules = set()
1029
+ else:
1030
+ submodules = set(submodules)
1031
+
1032
+ attr_to_modules = {attr: mod for mod, attrs in submod_attrs.items() for attr in attrs}
1033
+
1034
+ def __getattr__(name):
1035
+ if name in submodules:
1036
+ try:
1037
+ return importlib.import_module(f"{package_name}.{name}")
1038
+ except Exception as e:
1039
+ print(f"Error importing {package_name}.{name}: {e}")
1040
+ raise
1041
+ elif name in attr_to_modules:
1042
+ submod_path = f"{package_name}.{attr_to_modules[name]}"
1043
+ try:
1044
+ submod = importlib.import_module(submod_path)
1045
+ except Exception as e:
1046
+ print(f"Error importing {submod_path}: {e}")
1047
+ raise
1048
+ attr = getattr(submod, name)
1049
+
1050
+ # If the attribute lives in a file (module) with the same
1051
+ # name as the attribute, ensure that the attribute and *not*
1052
+ # the module is accessible on the package.
1053
+ if name == attr_to_modules[name]:
1054
+ pkg = sys.modules[package_name]
1055
+ pkg.__dict__[name] = attr
1056
+
1057
+ return attr
1058
+ else:
1059
+ raise AttributeError(f"No {package_name} attribute {name}")
1060
+
1061
+ def __dir__():
1062
+ return __all__
1063
+
1064
+ return __getattr__, __dir__
1065
+
1066
+
1067
+ __getattr__, __dir__ = _attach(__name__, submodules=[], submod_attrs=_SUBMOD_ATTRS)
1068
+
1069
+ if os.environ.get("EAGER_IMPORT", ""):
1070
+ for attr in __all__:
1071
+ __getattr__(attr)
1072
+
1073
+ # WARNING: any content below this statement is generated automatically. Any manual edit
1074
+ # will be lost when re-generating this file !
1075
+ #
1076
+ # To update the static imports, please run the following command and commit the changes.
1077
+ # ```
1078
+ # # Use script
1079
+ # python utils/check_static_imports.py --update
1080
+ #
1081
+ # # Or run style on codebase
1082
+ # make style
1083
+ # ```
1084
+ if TYPE_CHECKING: # pragma: no cover
1085
+ from ._commit_scheduler import CommitScheduler # noqa: F401
1086
+ from ._inference_endpoints import (
1087
+ InferenceEndpoint, # noqa: F401
1088
+ InferenceEndpointError, # noqa: F401
1089
+ InferenceEndpointStatus, # noqa: F401
1090
+ InferenceEndpointTimeoutError, # noqa: F401
1091
+ InferenceEndpointType, # noqa: F401
1092
+ )
1093
+ from ._jobs_api import (
1094
+ JobInfo, # noqa: F401
1095
+ JobOwner, # noqa: F401
1096
+ JobStage, # noqa: F401
1097
+ JobStatus, # noqa: F401
1098
+ )
1099
+ from ._login import (
1100
+ auth_list, # noqa: F401
1101
+ auth_switch, # noqa: F401
1102
+ interpreter_login, # noqa: F401
1103
+ login, # noqa: F401
1104
+ logout, # noqa: F401
1105
+ notebook_login, # noqa: F401
1106
+ )
1107
+ from ._oauth import (
1108
+ OAuthInfo, # noqa: F401
1109
+ OAuthOrgInfo, # noqa: F401
1110
+ OAuthUserInfo, # noqa: F401
1111
+ attach_huggingface_oauth, # noqa: F401
1112
+ parse_huggingface_oauth, # noqa: F401
1113
+ )
1114
+ from ._snapshot_download import snapshot_download # noqa: F401
1115
+ from ._space_api import (
1116
+ SpaceHardware, # noqa: F401
1117
+ SpaceRuntime, # noqa: F401
1118
+ SpaceStage, # noqa: F401
1119
+ SpaceStorage, # noqa: F401
1120
+ SpaceVariable, # noqa: F401
1121
+ )
1122
+ from ._tensorboard_logger import HFSummaryWriter # noqa: F401
1123
+ from ._webhooks_payload import (
1124
+ WebhookPayload, # noqa: F401
1125
+ WebhookPayloadComment, # noqa: F401
1126
+ WebhookPayloadDiscussion, # noqa: F401
1127
+ WebhookPayloadDiscussionChanges, # noqa: F401
1128
+ WebhookPayloadEvent, # noqa: F401
1129
+ WebhookPayloadMovedTo, # noqa: F401
1130
+ WebhookPayloadRepo, # noqa: F401
1131
+ WebhookPayloadUrl, # noqa: F401
1132
+ WebhookPayloadWebhook, # noqa: F401
1133
+ )
1134
+ from ._webhooks_server import (
1135
+ WebhooksServer, # noqa: F401
1136
+ webhook_endpoint, # noqa: F401
1137
+ )
1138
+ from .community import (
1139
+ Discussion, # noqa: F401
1140
+ DiscussionComment, # noqa: F401
1141
+ DiscussionCommit, # noqa: F401
1142
+ DiscussionEvent, # noqa: F401
1143
+ DiscussionStatusChange, # noqa: F401
1144
+ DiscussionTitleChange, # noqa: F401
1145
+ DiscussionWithDetails, # noqa: F401
1146
+ )
1147
+ from .constants import (
1148
+ CONFIG_NAME, # noqa: F401
1149
+ FLAX_WEIGHTS_NAME, # noqa: F401
1150
+ HUGGINGFACE_CO_URL_HOME, # noqa: F401
1151
+ HUGGINGFACE_CO_URL_TEMPLATE, # noqa: F401
1152
+ PYTORCH_WEIGHTS_NAME, # noqa: F401
1153
+ REPO_TYPE_DATASET, # noqa: F401
1154
+ REPO_TYPE_MODEL, # noqa: F401
1155
+ REPO_TYPE_SPACE, # noqa: F401
1156
+ TF2_WEIGHTS_NAME, # noqa: F401
1157
+ TF_WEIGHTS_NAME, # noqa: F401
1158
+ )
1159
+ from .fastai_utils import (
1160
+ _save_pretrained_fastai, # noqa: F401
1161
+ from_pretrained_fastai, # noqa: F401
1162
+ push_to_hub_fastai, # noqa: F401
1163
+ )
1164
+ from .file_download import (
1165
+ _CACHED_NO_EXIST, # noqa: F401
1166
+ HfFileMetadata, # noqa: F401
1167
+ get_hf_file_metadata, # noqa: F401
1168
+ hf_hub_download, # noqa: F401
1169
+ hf_hub_url, # noqa: F401
1170
+ try_to_load_from_cache, # noqa: F401
1171
+ )
1172
+ from .hf_api import (
1173
+ Collection, # noqa: F401
1174
+ CollectionItem, # noqa: F401
1175
+ CommitInfo, # noqa: F401
1176
+ CommitOperation, # noqa: F401
1177
+ CommitOperationAdd, # noqa: F401
1178
+ CommitOperationCopy, # noqa: F401
1179
+ CommitOperationDelete, # noqa: F401
1180
+ DatasetInfo, # noqa: F401
1181
+ GitCommitInfo, # noqa: F401
1182
+ GitRefInfo, # noqa: F401
1183
+ GitRefs, # noqa: F401
1184
+ HfApi, # noqa: F401
1185
+ ModelInfo, # noqa: F401
1186
+ Organization, # noqa: F401
1187
+ RepoUrl, # noqa: F401
1188
+ SpaceInfo, # noqa: F401
1189
+ User, # noqa: F401
1190
+ UserLikes, # noqa: F401
1191
+ WebhookInfo, # noqa: F401
1192
+ WebhookWatchedItem, # noqa: F401
1193
+ accept_access_request, # noqa: F401
1194
+ add_collection_item, # noqa: F401
1195
+ add_space_secret, # noqa: F401
1196
+ add_space_variable, # noqa: F401
1197
+ auth_check, # noqa: F401
1198
+ cancel_access_request, # noqa: F401
1199
+ cancel_job, # noqa: F401
1200
+ change_discussion_status, # noqa: F401
1201
+ comment_discussion, # noqa: F401
1202
+ create_branch, # noqa: F401
1203
+ create_collection, # noqa: F401
1204
+ create_commit, # noqa: F401
1205
+ create_discussion, # noqa: F401
1206
+ create_inference_endpoint, # noqa: F401
1207
+ create_inference_endpoint_from_catalog, # noqa: F401
1208
+ create_pull_request, # noqa: F401
1209
+ create_repo, # noqa: F401
1210
+ create_scheduled_job, # noqa: F401
1211
+ create_scheduled_uv_job, # noqa: F401
1212
+ create_tag, # noqa: F401
1213
+ create_webhook, # noqa: F401
1214
+ dataset_info, # noqa: F401
1215
+ delete_branch, # noqa: F401
1216
+ delete_collection, # noqa: F401
1217
+ delete_collection_item, # noqa: F401
1218
+ delete_file, # noqa: F401
1219
+ delete_folder, # noqa: F401
1220
+ delete_inference_endpoint, # noqa: F401
1221
+ delete_repo, # noqa: F401
1222
+ delete_scheduled_job, # noqa: F401
1223
+ delete_space_secret, # noqa: F401
1224
+ delete_space_storage, # noqa: F401
1225
+ delete_space_variable, # noqa: F401
1226
+ delete_tag, # noqa: F401
1227
+ delete_webhook, # noqa: F401
1228
+ disable_webhook, # noqa: F401
1229
+ duplicate_space, # noqa: F401
1230
+ edit_discussion_comment, # noqa: F401
1231
+ enable_webhook, # noqa: F401
1232
+ fetch_job_logs, # noqa: F401
1233
+ file_exists, # noqa: F401
1234
+ get_collection, # noqa: F401
1235
+ get_dataset_tags, # noqa: F401
1236
+ get_discussion_details, # noqa: F401
1237
+ get_full_repo_name, # noqa: F401
1238
+ get_inference_endpoint, # noqa: F401
1239
+ get_model_tags, # noqa: F401
1240
+ get_organization_overview, # noqa: F401
1241
+ get_paths_info, # noqa: F401
1242
+ get_repo_discussions, # noqa: F401
1243
+ get_safetensors_metadata, # noqa: F401
1244
+ get_space_runtime, # noqa: F401
1245
+ get_space_variables, # noqa: F401
1246
+ get_token_permission, # noqa: F401
1247
+ get_user_overview, # noqa: F401
1248
+ get_webhook, # noqa: F401
1249
+ grant_access, # noqa: F401
1250
+ inspect_job, # noqa: F401
1251
+ inspect_scheduled_job, # noqa: F401
1252
+ list_accepted_access_requests, # noqa: F401
1253
+ list_collections, # noqa: F401
1254
+ list_datasets, # noqa: F401
1255
+ list_inference_catalog, # noqa: F401
1256
+ list_inference_endpoints, # noqa: F401
1257
+ list_jobs, # noqa: F401
1258
+ list_lfs_files, # noqa: F401
1259
+ list_liked_repos, # noqa: F401
1260
+ list_models, # noqa: F401
1261
+ list_organization_members, # noqa: F401
1262
+ list_papers, # noqa: F401
1263
+ list_pending_access_requests, # noqa: F401
1264
+ list_rejected_access_requests, # noqa: F401
1265
+ list_repo_commits, # noqa: F401
1266
+ list_repo_files, # noqa: F401
1267
+ list_repo_likers, # noqa: F401
1268
+ list_repo_refs, # noqa: F401
1269
+ list_repo_tree, # noqa: F401
1270
+ list_spaces, # noqa: F401
1271
+ list_user_followers, # noqa: F401
1272
+ list_user_following, # noqa: F401
1273
+ list_webhooks, # noqa: F401
1274
+ merge_pull_request, # noqa: F401
1275
+ model_info, # noqa: F401
1276
+ move_repo, # noqa: F401
1277
+ paper_info, # noqa: F401
1278
+ parse_safetensors_file_metadata, # noqa: F401
1279
+ pause_inference_endpoint, # noqa: F401
1280
+ pause_space, # noqa: F401
1281
+ permanently_delete_lfs_files, # noqa: F401
1282
+ preupload_lfs_files, # noqa: F401
1283
+ reject_access_request, # noqa: F401
1284
+ rename_discussion, # noqa: F401
1285
+ repo_exists, # noqa: F401
1286
+ repo_info, # noqa: F401
1287
+ repo_type_and_id_from_hf_id, # noqa: F401
1288
+ request_space_hardware, # noqa: F401
1289
+ request_space_storage, # noqa: F401
1290
+ restart_space, # noqa: F401
1291
+ resume_inference_endpoint, # noqa: F401
1292
+ resume_scheduled_job, # noqa: F401
1293
+ revision_exists, # noqa: F401
1294
+ run_as_future, # noqa: F401
1295
+ run_job, # noqa: F401
1296
+ run_uv_job, # noqa: F401
1297
+ scale_to_zero_inference_endpoint, # noqa: F401
1298
+ set_space_sleep_time, # noqa: F401
1299
+ space_info, # noqa: F401
1300
+ super_squash_history, # noqa: F401
1301
+ suspend_scheduled_job, # noqa: F401
1302
+ unlike, # noqa: F401
1303
+ update_collection_item, # noqa: F401
1304
+ update_collection_metadata, # noqa: F401
1305
+ update_inference_endpoint, # noqa: F401
1306
+ update_repo_settings, # noqa: F401
1307
+ update_repo_visibility, # noqa: F401
1308
+ update_webhook, # noqa: F401
1309
+ upload_file, # noqa: F401
1310
+ upload_folder, # noqa: F401
1311
+ upload_large_folder, # noqa: F401
1312
+ whoami, # noqa: F401
1313
+ )
1314
+ from .hf_file_system import (
1315
+ HfFileSystem, # noqa: F401
1316
+ HfFileSystemFile, # noqa: F401
1317
+ HfFileSystemResolvedPath, # noqa: F401
1318
+ HfFileSystemStreamFile, # noqa: F401
1319
+ )
1320
+ from .hub_mixin import (
1321
+ ModelHubMixin, # noqa: F401
1322
+ PyTorchModelHubMixin, # noqa: F401
1323
+ )
1324
+ from .inference._client import (
1325
+ InferenceClient, # noqa: F401
1326
+ InferenceTimeoutError, # noqa: F401
1327
+ )
1328
+ from .inference._generated._async_client import AsyncInferenceClient # noqa: F401
1329
+ from .inference._generated.types import (
1330
+ AudioClassificationInput, # noqa: F401
1331
+ AudioClassificationOutputElement, # noqa: F401
1332
+ AudioClassificationOutputTransform, # noqa: F401
1333
+ AudioClassificationParameters, # noqa: F401
1334
+ AudioToAudioInput, # noqa: F401
1335
+ AudioToAudioOutputElement, # noqa: F401
1336
+ AutomaticSpeechRecognitionEarlyStoppingEnum, # noqa: F401
1337
+ AutomaticSpeechRecognitionGenerationParameters, # noqa: F401
1338
+ AutomaticSpeechRecognitionInput, # noqa: F401
1339
+ AutomaticSpeechRecognitionOutput, # noqa: F401
1340
+ AutomaticSpeechRecognitionOutputChunk, # noqa: F401
1341
+ AutomaticSpeechRecognitionParameters, # noqa: F401
1342
+ ChatCompletionInput, # noqa: F401
1343
+ ChatCompletionInputFunctionDefinition, # noqa: F401
1344
+ ChatCompletionInputFunctionName, # noqa: F401
1345
+ ChatCompletionInputGrammarType, # noqa: F401
1346
+ ChatCompletionInputJSONSchema, # noqa: F401
1347
+ ChatCompletionInputMessage, # noqa: F401
1348
+ ChatCompletionInputMessageChunk, # noqa: F401
1349
+ ChatCompletionInputMessageChunkType, # noqa: F401
1350
+ ChatCompletionInputResponseFormatJSONObject, # noqa: F401
1351
+ ChatCompletionInputResponseFormatJSONSchema, # noqa: F401
1352
+ ChatCompletionInputResponseFormatText, # noqa: F401
1353
+ ChatCompletionInputStreamOptions, # noqa: F401
1354
+ ChatCompletionInputTool, # noqa: F401
1355
+ ChatCompletionInputToolCall, # noqa: F401
1356
+ ChatCompletionInputToolChoiceClass, # noqa: F401
1357
+ ChatCompletionInputToolChoiceEnum, # noqa: F401
1358
+ ChatCompletionInputURL, # noqa: F401
1359
+ ChatCompletionOutput, # noqa: F401
1360
+ ChatCompletionOutputComplete, # noqa: F401
1361
+ ChatCompletionOutputFunctionDefinition, # noqa: F401
1362
+ ChatCompletionOutputLogprob, # noqa: F401
1363
+ ChatCompletionOutputLogprobs, # noqa: F401
1364
+ ChatCompletionOutputMessage, # noqa: F401
1365
+ ChatCompletionOutputToolCall, # noqa: F401
1366
+ ChatCompletionOutputTopLogprob, # noqa: F401
1367
+ ChatCompletionOutputUsage, # noqa: F401
1368
+ ChatCompletionStreamOutput, # noqa: F401
1369
+ ChatCompletionStreamOutputChoice, # noqa: F401
1370
+ ChatCompletionStreamOutputDelta, # noqa: F401
1371
+ ChatCompletionStreamOutputDeltaToolCall, # noqa: F401
1372
+ ChatCompletionStreamOutputFunction, # noqa: F401
1373
+ ChatCompletionStreamOutputLogprob, # noqa: F401
1374
+ ChatCompletionStreamOutputLogprobs, # noqa: F401
1375
+ ChatCompletionStreamOutputTopLogprob, # noqa: F401
1376
+ ChatCompletionStreamOutputUsage, # noqa: F401
1377
+ DepthEstimationInput, # noqa: F401
1378
+ DepthEstimationOutput, # noqa: F401
1379
+ DocumentQuestionAnsweringInput, # noqa: F401
1380
+ DocumentQuestionAnsweringInputData, # noqa: F401
1381
+ DocumentQuestionAnsweringOutputElement, # noqa: F401
1382
+ DocumentQuestionAnsweringParameters, # noqa: F401
1383
+ FeatureExtractionInput, # noqa: F401
1384
+ FeatureExtractionInputTruncationDirection, # noqa: F401
1385
+ FillMaskInput, # noqa: F401
1386
+ FillMaskOutputElement, # noqa: F401
1387
+ FillMaskParameters, # noqa: F401
1388
+ ImageClassificationInput, # noqa: F401
1389
+ ImageClassificationOutputElement, # noqa: F401
1390
+ ImageClassificationOutputTransform, # noqa: F401
1391
+ ImageClassificationParameters, # noqa: F401
1392
+ ImageSegmentationInput, # noqa: F401
1393
+ ImageSegmentationOutputElement, # noqa: F401
1394
+ ImageSegmentationParameters, # noqa: F401
1395
+ ImageSegmentationSubtask, # noqa: F401
1396
+ ImageToImageInput, # noqa: F401
1397
+ ImageToImageOutput, # noqa: F401
1398
+ ImageToImageParameters, # noqa: F401
1399
+ ImageToImageTargetSize, # noqa: F401
1400
+ ImageToTextEarlyStoppingEnum, # noqa: F401
1401
+ ImageToTextGenerationParameters, # noqa: F401
1402
+ ImageToTextInput, # noqa: F401
1403
+ ImageToTextOutput, # noqa: F401
1404
+ ImageToTextParameters, # noqa: F401
1405
+ ImageToVideoInput, # noqa: F401
1406
+ ImageToVideoOutput, # noqa: F401
1407
+ ImageToVideoParameters, # noqa: F401
1408
+ ImageToVideoTargetSize, # noqa: F401
1409
+ ObjectDetectionBoundingBox, # noqa: F401
1410
+ ObjectDetectionInput, # noqa: F401
1411
+ ObjectDetectionOutputElement, # noqa: F401
1412
+ ObjectDetectionParameters, # noqa: F401
1413
+ Padding, # noqa: F401
1414
+ QuestionAnsweringInput, # noqa: F401
1415
+ QuestionAnsweringInputData, # noqa: F401
1416
+ QuestionAnsweringOutputElement, # noqa: F401
1417
+ QuestionAnsweringParameters, # noqa: F401
1418
+ SentenceSimilarityInput, # noqa: F401
1419
+ SentenceSimilarityInputData, # noqa: F401
1420
+ SummarizationInput, # noqa: F401
1421
+ SummarizationOutput, # noqa: F401
1422
+ SummarizationParameters, # noqa: F401
1423
+ SummarizationTruncationStrategy, # noqa: F401
1424
+ TableQuestionAnsweringInput, # noqa: F401
1425
+ TableQuestionAnsweringInputData, # noqa: F401
1426
+ TableQuestionAnsweringOutputElement, # noqa: F401
1427
+ TableQuestionAnsweringParameters, # noqa: F401
1428
+ Text2TextGenerationInput, # noqa: F401
1429
+ Text2TextGenerationOutput, # noqa: F401
1430
+ Text2TextGenerationParameters, # noqa: F401
1431
+ Text2TextGenerationTruncationStrategy, # noqa: F401
1432
+ TextClassificationInput, # noqa: F401
1433
+ TextClassificationOutputElement, # noqa: F401
1434
+ TextClassificationOutputTransform, # noqa: F401
1435
+ TextClassificationParameters, # noqa: F401
1436
+ TextGenerationInput, # noqa: F401
1437
+ TextGenerationInputGenerateParameters, # noqa: F401
1438
+ TextGenerationInputGrammarType, # noqa: F401
1439
+ TextGenerationOutput, # noqa: F401
1440
+ TextGenerationOutputBestOfSequence, # noqa: F401
1441
+ TextGenerationOutputDetails, # noqa: F401
1442
+ TextGenerationOutputFinishReason, # noqa: F401
1443
+ TextGenerationOutputPrefillToken, # noqa: F401
1444
+ TextGenerationOutputToken, # noqa: F401
1445
+ TextGenerationStreamOutput, # noqa: F401
1446
+ TextGenerationStreamOutputStreamDetails, # noqa: F401
1447
+ TextGenerationStreamOutputToken, # noqa: F401
1448
+ TextToAudioEarlyStoppingEnum, # noqa: F401
1449
+ TextToAudioGenerationParameters, # noqa: F401
1450
+ TextToAudioInput, # noqa: F401
1451
+ TextToAudioOutput, # noqa: F401
1452
+ TextToAudioParameters, # noqa: F401
1453
+ TextToImageInput, # noqa: F401
1454
+ TextToImageOutput, # noqa: F401
1455
+ TextToImageParameters, # noqa: F401
1456
+ TextToSpeechEarlyStoppingEnum, # noqa: F401
1457
+ TextToSpeechGenerationParameters, # noqa: F401
1458
+ TextToSpeechInput, # noqa: F401
1459
+ TextToSpeechOutput, # noqa: F401
1460
+ TextToSpeechParameters, # noqa: F401
1461
+ TextToVideoInput, # noqa: F401
1462
+ TextToVideoOutput, # noqa: F401
1463
+ TextToVideoParameters, # noqa: F401
1464
+ TokenClassificationAggregationStrategy, # noqa: F401
1465
+ TokenClassificationInput, # noqa: F401
1466
+ TokenClassificationOutputElement, # noqa: F401
1467
+ TokenClassificationParameters, # noqa: F401
1468
+ TranslationInput, # noqa: F401
1469
+ TranslationOutput, # noqa: F401
1470
+ TranslationParameters, # noqa: F401
1471
+ TranslationTruncationStrategy, # noqa: F401
1472
+ TypeEnum, # noqa: F401
1473
+ VideoClassificationInput, # noqa: F401
1474
+ VideoClassificationOutputElement, # noqa: F401
1475
+ VideoClassificationOutputTransform, # noqa: F401
1476
+ VideoClassificationParameters, # noqa: F401
1477
+ VisualQuestionAnsweringInput, # noqa: F401
1478
+ VisualQuestionAnsweringInputData, # noqa: F401
1479
+ VisualQuestionAnsweringOutputElement, # noqa: F401
1480
+ VisualQuestionAnsweringParameters, # noqa: F401
1481
+ ZeroShotClassificationInput, # noqa: F401
1482
+ ZeroShotClassificationOutputElement, # noqa: F401
1483
+ ZeroShotClassificationParameters, # noqa: F401
1484
+ ZeroShotImageClassificationInput, # noqa: F401
1485
+ ZeroShotImageClassificationOutputElement, # noqa: F401
1486
+ ZeroShotImageClassificationParameters, # noqa: F401
1487
+ ZeroShotObjectDetectionBoundingBox, # noqa: F401
1488
+ ZeroShotObjectDetectionInput, # noqa: F401
1489
+ ZeroShotObjectDetectionOutputElement, # noqa: F401
1490
+ ZeroShotObjectDetectionParameters, # noqa: F401
1491
+ )
1492
+ from .inference._mcp.agent import Agent # noqa: F401
1493
+ from .inference._mcp.mcp_client import MCPClient # noqa: F401
1494
+ from .inference_api import InferenceApi # noqa: F401
1495
+ from .keras_mixin import (
1496
+ KerasModelHubMixin, # noqa: F401
1497
+ from_pretrained_keras, # noqa: F401
1498
+ push_to_hub_keras, # noqa: F401
1499
+ save_pretrained_keras, # noqa: F401
1500
+ )
1501
+ from .repocard import (
1502
+ DatasetCard, # noqa: F401
1503
+ ModelCard, # noqa: F401
1504
+ RepoCard, # noqa: F401
1505
+ SpaceCard, # noqa: F401
1506
+ metadata_eval_result, # noqa: F401
1507
+ metadata_load, # noqa: F401
1508
+ metadata_save, # noqa: F401
1509
+ metadata_update, # noqa: F401
1510
+ )
1511
+ from .repocard_data import (
1512
+ CardData, # noqa: F401
1513
+ DatasetCardData, # noqa: F401
1514
+ EvalResult, # noqa: F401
1515
+ ModelCardData, # noqa: F401
1516
+ SpaceCardData, # noqa: F401
1517
+ )
1518
+ from .repository import Repository # noqa: F401
1519
+ from .serialization import (
1520
+ StateDictSplit, # noqa: F401
1521
+ get_tf_storage_size, # noqa: F401
1522
+ get_torch_storage_id, # noqa: F401
1523
+ get_torch_storage_size, # noqa: F401
1524
+ load_state_dict_from_file, # noqa: F401
1525
+ load_torch_model, # noqa: F401
1526
+ save_torch_model, # noqa: F401
1527
+ save_torch_state_dict, # noqa: F401
1528
+ split_state_dict_into_shards_factory, # noqa: F401
1529
+ split_tf_state_dict_into_shards, # noqa: F401
1530
+ split_torch_state_dict_into_shards, # noqa: F401
1531
+ )
1532
+ from .serialization._dduf import (
1533
+ DDUFEntry, # noqa: F401
1534
+ export_entries_as_dduf, # noqa: F401
1535
+ export_folder_as_dduf, # noqa: F401
1536
+ read_dduf_file, # noqa: F401
1537
+ )
1538
+ from .utils import (
1539
+ CachedFileInfo, # noqa: F401
1540
+ CachedRepoInfo, # noqa: F401
1541
+ CachedRevisionInfo, # noqa: F401
1542
+ CacheNotFound, # noqa: F401
1543
+ CorruptedCacheException, # noqa: F401
1544
+ DeleteCacheStrategy, # noqa: F401
1545
+ HFCacheInfo, # noqa: F401
1546
+ HfFolder, # noqa: F401
1547
+ cached_assets_path, # noqa: F401
1548
+ configure_http_backend, # noqa: F401
1549
+ dump_environment_info, # noqa: F401
1550
+ get_session, # noqa: F401
1551
+ get_token, # noqa: F401
1552
+ logging, # noqa: F401
1553
+ scan_cache_dir, # noqa: F401
1554
+ )
.venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (27.8 kB). View file
 
.venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_commit_api.cpython-311.pyc ADDED
Binary file (47.9 kB). View file
 
.venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_inference_endpoints.cpython-311.pyc ADDED
Binary file (21 kB). View file
 
.venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_jobs_api.cpython-311.pyc ADDED
Binary file (15.9 kB). View file
 
.venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_local_folder.cpython-311.pyc ADDED
Binary file (22.8 kB). View file