Instructions to use fuuuzzy/F5-TTS-pt-br with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- F5-TTS
How to use fuuuzzy/F5-TTS-pt-br with F5-TTS:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +18 -0
- .venv/bin/ruff +3 -0
- .venv/lib/python3.11/site-packages/dill/__pycache__/_dill.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/git/__init__.py +300 -0
- .venv/lib/python3.11/site-packages/git/cmd.py +1741 -0
- .venv/lib/python3.11/site-packages/git/compat.py +165 -0
- .venv/lib/python3.11/site-packages/git/config.py +951 -0
- .venv/lib/python3.11/site-packages/git/db.py +71 -0
- .venv/lib/python3.11/site-packages/git/diff.py +775 -0
- .venv/lib/python3.11/site-packages/git/exc.py +228 -0
- .venv/lib/python3.11/site-packages/git/index/__init__.py +16 -0
- .venv/lib/python3.11/site-packages/git/index/base.py +1532 -0
- .venv/lib/python3.11/site-packages/git/index/fun.py +465 -0
- .venv/lib/python3.11/site-packages/git/index/typ.py +202 -0
- .venv/lib/python3.11/site-packages/git/index/util.py +121 -0
- .venv/lib/python3.11/site-packages/git/objects/__init__.py +25 -0
- .venv/lib/python3.11/site-packages/git/objects/base.py +301 -0
- .venv/lib/python3.11/site-packages/git/objects/blob.py +48 -0
- .venv/lib/python3.11/site-packages/git/objects/commit.py +909 -0
- .venv/lib/python3.11/site-packages/git/objects/fun.py +281 -0
- .venv/lib/python3.11/site-packages/git/objects/submodule/__init__.py +7 -0
- .venv/lib/python3.11/site-packages/git/objects/submodule/base.py +1642 -0
- .venv/lib/python3.11/site-packages/git/objects/submodule/root.py +467 -0
- .venv/lib/python3.11/site-packages/git/objects/submodule/util.py +121 -0
- .venv/lib/python3.11/site-packages/git/objects/tag.py +140 -0
- .venv/lib/python3.11/site-packages/git/objects/tree.py +416 -0
- .venv/lib/python3.11/site-packages/git/objects/util.py +700 -0
- .venv/lib/python3.11/site-packages/git/py.typed +0 -0
- .venv/lib/python3.11/site-packages/git/refs/__init__.py +21 -0
- .venv/lib/python3.11/site-packages/git/refs/head.py +304 -0
- .venv/lib/python3.11/site-packages/git/refs/log.py +399 -0
- .venv/lib/python3.11/site-packages/git/refs/reference.py +176 -0
- .venv/lib/python3.11/site-packages/git/refs/remote.py +79 -0
- .venv/lib/python3.11/site-packages/git/refs/symbolic.py +933 -0
- .venv/lib/python3.11/site-packages/git/refs/tag.py +155 -0
- .venv/lib/python3.11/site-packages/git/remote.py +1248 -0
- .venv/lib/python3.11/site-packages/git/repo/__init__.py +8 -0
- .venv/lib/python3.11/site-packages/git/repo/base.py +1638 -0
- .venv/lib/python3.11/site-packages/git/repo/fun.py +425 -0
- .venv/lib/python3.11/site-packages/git/types.py +285 -0
- .venv/lib/python3.11/site-packages/git/util.py +1350 -0
- .venv/lib/python3.11/site-packages/google/_upb/_message.abi3.so +3 -0
- .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/blob.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/google/cloud/storage/__pycache__/bucket.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/huggingface_hub/__init__.py +1554 -0
- .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_commit_api.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_inference_endpoints.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/huggingface_hub/__pycache__/_jobs_api.cpython-311.pyc +0 -0
- .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
|
|
|