SuperRealCo commited on
Commit
7a26217
·
verified ·
1 Parent(s): bbadb3d

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/AUTHORS +59 -0
  2. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/INSTALLER +1 -0
  3. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/LICENSE +29 -0
  4. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/METADATA +295 -0
  5. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/RECORD +83 -0
  6. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/REQUESTED +0 -0
  7. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/WHEEL +5 -0
  8. venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/top_level.txt +1 -0
  9. venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/RECORD +175 -0
  10. venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/WHEEL +5 -0
  11. venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE +202 -0
  12. venv/lib/python3.10/site-packages/git/__init__.py +300 -0
  13. venv/lib/python3.10/site-packages/git/__pycache__/__init__.cpython-310.pyc +0 -0
  14. venv/lib/python3.10/site-packages/git/__pycache__/cmd.cpython-310.pyc +0 -0
  15. venv/lib/python3.10/site-packages/git/__pycache__/compat.cpython-310.pyc +0 -0
  16. venv/lib/python3.10/site-packages/git/__pycache__/config.cpython-310.pyc +0 -0
  17. venv/lib/python3.10/site-packages/git/__pycache__/db.cpython-310.pyc +0 -0
  18. venv/lib/python3.10/site-packages/git/__pycache__/diff.cpython-310.pyc +0 -0
  19. venv/lib/python3.10/site-packages/git/__pycache__/exc.cpython-310.pyc +0 -0
  20. venv/lib/python3.10/site-packages/git/__pycache__/remote.cpython-310.pyc +0 -0
  21. venv/lib/python3.10/site-packages/git/__pycache__/types.cpython-310.pyc +0 -0
  22. venv/lib/python3.10/site-packages/git/__pycache__/util.cpython-310.pyc +0 -0
  23. venv/lib/python3.10/site-packages/git/cmd.py +1724 -0
  24. venv/lib/python3.10/site-packages/git/compat.py +165 -0
  25. venv/lib/python3.10/site-packages/git/config.py +944 -0
  26. venv/lib/python3.10/site-packages/git/db.py +71 -0
  27. venv/lib/python3.10/site-packages/git/diff.py +775 -0
  28. venv/lib/python3.10/site-packages/git/exc.py +228 -0
  29. venv/lib/python3.10/site-packages/git/index/typ.py +202 -0
  30. venv/lib/python3.10/site-packages/git/index/util.py +121 -0
  31. venv/lib/python3.10/site-packages/git/objects/__init__.py +25 -0
  32. venv/lib/python3.10/site-packages/git/objects/__pycache__/base.cpython-310.pyc +0 -0
  33. venv/lib/python3.10/site-packages/git/objects/__pycache__/fun.cpython-310.pyc +0 -0
  34. venv/lib/python3.10/site-packages/git/objects/__pycache__/tag.cpython-310.pyc +0 -0
  35. venv/lib/python3.10/site-packages/git/objects/__pycache__/tree.cpython-310.pyc +0 -0
  36. venv/lib/python3.10/site-packages/git/objects/base.py +301 -0
  37. venv/lib/python3.10/site-packages/git/objects/blob.py +48 -0
  38. venv/lib/python3.10/site-packages/git/objects/commit.py +909 -0
  39. venv/lib/python3.10/site-packages/git/objects/fun.py +281 -0
  40. venv/lib/python3.10/site-packages/git/objects/submodule/__init__.py +7 -0
  41. venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/__init__.cpython-310.pyc +0 -0
  42. venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/base.cpython-310.pyc +0 -0
  43. venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/root.cpython-310.pyc +0 -0
  44. venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/util.cpython-310.pyc +0 -0
  45. venv/lib/python3.10/site-packages/git/objects/submodule/base.py +1632 -0
  46. venv/lib/python3.10/site-packages/git/objects/submodule/root.py +467 -0
  47. venv/lib/python3.10/site-packages/git/objects/submodule/util.py +121 -0
  48. venv/lib/python3.10/site-packages/git/objects/tag.py +140 -0
  49. venv/lib/python3.10/site-packages/git/objects/tree.py +414 -0
  50. venv/lib/python3.10/site-packages/git/objects/util.py +700 -0
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/AUTHORS ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GitPython was originally written by Michael Trier.
2
+ GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich.
3
+
4
+ Contributors are:
5
+
6
+ -Michael Trier <mtrier _at_ gmail.com>
7
+ -Alan Briolat
8
+ -Florian Apolloner <florian _at_ apolloner.eu>
9
+ -David Aguilar <davvid _at_ gmail.com>
10
+ -Jelmer Vernooij <jelmer _at_ samba.org>
11
+ -Steve Frécinaux <code _at_ istique.net>
12
+ -Kai Lautaportti <kai _at_ lautaportti.fi>
13
+ -Paul Sowden <paul _at_ idontsmoke.co.uk>
14
+ -Sebastian Thiel <byronimo _at_ gmail.com>
15
+ -Jonathan Chu <jonathan.chu _at_ me.com>
16
+ -Vincent Driessen <me _at_ nvie.com>
17
+ -Phil Elson <pelson _dot_ pub _at_ gmail.com>
18
+ -Bernard `Guyzmo` Pratz <guyzmo+gitpython+pub@m0g.net>
19
+ -Timothy B. Hartman <tbhartman _at_ gmail.com>
20
+ -Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
21
+ -Peter Jones <pjones _at_ redhat.com>
22
+ -Anson Mansfield <anson.mansfield _at_ gmail.com>
23
+ -Ken Odegard <ken.odegard _at_ gmail.com>
24
+ -Alexis Horgix Chotard
25
+ -Piotr Babij <piotr.babij _at_ gmail.com>
26
+ -Mikuláš Poul <mikulaspoul _at_ gmail.com>
27
+ -Charles Bouchard-Légaré <cblegare.atl _at_ ntis.ca>
28
+ -Yaroslav Halchenko <debian _at_ onerussian.com>
29
+ -Tim Swast <swast _at_ google.com>
30
+ -William Luc Ritchie
31
+ -David Host <hostdm _at_ outlook.com>
32
+ -A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
33
+ -Steven Whitman <ninloot _at_ gmail.com>
34
+ -Stefan Stancu <stefan.stancu _at_ gmail.com>
35
+ -César Izurieta <cesar _at_ caih.org>
36
+ -Arthur Milchior <arthur _at_ milchior.fr>
37
+ -Anil Khatri <anil.soccer.khatri _at_ gmail.com>
38
+ -JJ Graham <thetwoj _at_ gmail.com>
39
+ -Ben Thayer <ben _at_ benthayer.com>
40
+ -Dries Kennes <admin _at_ dries007.net>
41
+ -Pratik Anurag <panurag247365 _at_ gmail.com>
42
+ -Harmon <harmon.public _at_ gmail.com>
43
+ -Liam Beguin <liambeguin _at_ gmail.com>
44
+ -Ram Rachum <ram _at_ rachum.com>
45
+ -Alba Mendez <me _at_ alba.sh>
46
+ -Robert Westman <robert _at_ byteflux.io>
47
+ -Hugo van Kemenade
48
+ -Hiroki Tokunaga <tokusan441 _at_ gmail.com>
49
+ -Julien Mauroy <pro.julien.mauroy _at_ gmail.com>
50
+ -Patrick Gerard
51
+ -Luke Twist <itsluketwist@gmail.com>
52
+ -Joseph Hale <me _at_ jhale.dev>
53
+ -Santos Gallegos <stsewd _at_ proton.me>
54
+ -Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
55
+ -Eliah Kagan <eliah.kagan _at_ gmail.com>
56
+ -Ethan Lin <et.repositories _at_ gmail.com>
57
+ -Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
58
+
59
+ Portions derived from other open source works and are clearly marked.
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/LICENSE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (C) 2008, 2009 Michael Trier and contributors
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+
15
+ * Neither the name of the GitPython project nor the names of
16
+ its contributors may be used to endorse or promote products derived
17
+ from this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/METADATA ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: GitPython
3
+ Version: 3.1.44
4
+ Summary: GitPython is a Python library used to interact with Git repositories
5
+ Home-page: https://github.com/gitpython-developers/GitPython
6
+ Author: Sebastian Thiel, Michael Trier
7
+ Author-email: byronimo@gmail.com, mtrier@gmail.com
8
+ License: BSD-3-Clause
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Typing :: Typed
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Requires-Python: >=3.7
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ License-File: AUTHORS
30
+ Requires-Dist: gitdb<5,>=4.0.1
31
+ Requires-Dist: typing-extensions>=3.7.4.3; python_version < "3.8"
32
+ Provides-Extra: test
33
+ Requires-Dist: coverage[toml]; extra == "test"
34
+ Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
35
+ Requires-Dist: mock; python_version < "3.8" and extra == "test"
36
+ Requires-Dist: mypy; extra == "test"
37
+ Requires-Dist: pre-commit; extra == "test"
38
+ Requires-Dist: pytest>=7.3.1; extra == "test"
39
+ Requires-Dist: pytest-cov; extra == "test"
40
+ Requires-Dist: pytest-instafail; extra == "test"
41
+ Requires-Dist: pytest-mock; extra == "test"
42
+ Requires-Dist: pytest-sugar; extra == "test"
43
+ Requires-Dist: typing-extensions; python_version < "3.11" and extra == "test"
44
+ Provides-Extra: doc
45
+ Requires-Dist: sphinx<7.2,>=7.1.2; extra == "doc"
46
+ Requires-Dist: sphinx_rtd_theme; extra == "doc"
47
+ Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
48
+
49
+ ![Python package](https://github.com/gitpython-developers/GitPython/workflows/Python%20package/badge.svg)
50
+ [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
51
+ [![Packaging status](https://repology.org/badge/tiny-repos/python:gitpython.svg)](https://repology.org/metapackage/python:gitpython/versions)
52
+
53
+ ## [Gitoxide](https://github.com/Byron/gitoxide): A peek into the future…
54
+
55
+ I started working on GitPython in 2009, back in the days when Python was 'my thing' and I had great plans with it.
56
+ Of course, back in the days, I didn't really know what I was doing and this shows in many places. Somewhat similar to
57
+ Python this happens to be 'good enough', but at the same time is deeply flawed and broken beyond repair.
58
+
59
+ By now, GitPython is widely used and I am sure there is a good reason for that, it's something to be proud of and happy about.
60
+ The community is maintaining the software and is keeping it relevant for which I am absolutely grateful. For the time to come I am happy to continue maintaining GitPython, remaining hopeful that one day it won't be needed anymore.
61
+
62
+ More than 15 years after my first meeting with 'git' I am still in excited about it, and am happy to finally have the tools and
63
+ probably the skills to scratch that itch of mine: implement `git` in a way that makes tool creation a piece of cake for most.
64
+
65
+ If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
66
+ implementation of 'git' in [Rust](https://www.rust-lang.org).
67
+
68
+ *(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)*
69
+
70
+ ## GitPython
71
+
72
+ GitPython is a python library used to interact with git repositories, high-level like git-porcelain,
73
+ or low-level like git-plumbing.
74
+
75
+ It provides abstractions of git objects for easy access of repository data often backed by calling the `git`
76
+ command-line program.
77
+
78
+ ### DEVELOPMENT STATUS
79
+
80
+ This project is in **maintenance mode**, which means that
81
+
82
+ - …there will be no feature development, unless these are contributed
83
+ - …there will be no bug fixes, unless they are relevant to the safety of users, or contributed
84
+ - …issues will be responded to with waiting times of up to a month
85
+
86
+ The project is open to contributions of all kinds, as well as new maintainers.
87
+
88
+ ### REQUIREMENTS
89
+
90
+ GitPython needs the `git` executable to be installed on the system and available in your
91
+ `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it
92
+ by setting the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
93
+
94
+ - Git (1.7.x or newer)
95
+ - Python >= 3.7
96
+
97
+ The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
98
+ The installer takes care of installing them for you.
99
+
100
+ ### INSTALL
101
+
102
+ GitPython and its required package dependencies can be installed in any of the following ways, all of which should typically be done in a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
103
+
104
+ #### From PyPI
105
+
106
+ To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run:
107
+
108
+ ```sh
109
+ pip install GitPython
110
+ ```
111
+
112
+ (A distribution package can also be downloaded for manual installation at [the PyPI page](https://pypi.org/project/GitPython/).)
113
+
114
+ #### From downloaded source code
115
+
116
+ If you have downloaded the source code, run this from inside the unpacked `GitPython` directory:
117
+
118
+ ```sh
119
+ pip install .
120
+ ```
121
+
122
+ #### By cloning the source code repository
123
+
124
+ To clone the [the GitHub repository](https://github.com/gitpython-developers/GitPython) from source to work on the code, you can do it like so:
125
+
126
+ ```sh
127
+ git clone https://github.com/gitpython-developers/GitPython
128
+ cd GitPython
129
+ ./init-tests-after-clone.sh
130
+ ```
131
+
132
+ On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
133
+
134
+ If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`):
135
+
136
+ ```sh
137
+ gh repo clone GitPython
138
+ ```
139
+
140
+ Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html).
141
+
142
+ Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs):
143
+
144
+ ```sh
145
+ pip install -e ".[test]"
146
+ ```
147
+
148
+ In the less common case that you do not want to install test dependencies, `pip install -e .` can be used instead.
149
+
150
+ #### With editable *dependencies* (not preferred, and rarely needed)
151
+
152
+ In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediately reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
153
+
154
+ If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
155
+
156
+ ```sh
157
+ pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
158
+ ```
159
+
160
+ The submodules must have been cloned for that to work, but that will already be the case if you have run `./init-tests-after-clone.sh`. You can use `pip list` to check which packages are installed editably and which are installed normally.
161
+
162
+ To reiterate, this approach should only rarely be used. For most development it is preferable to allow the gitdb and smmap dependencices to be retrieved automatically from PyPI in their latest stable packaged versions.
163
+
164
+ ### Limitations
165
+
166
+ #### Leakage of System Resources
167
+
168
+ GitPython is not suited for long-running processes (like daemons) as it tends to
169
+ leak system resources. It was written in a time where destructors (as implemented
170
+ in the `__del__` method) still ran deterministically.
171
+
172
+ In case you still want to use it in such a context, you will want to search the
173
+ codebase for `__del__` implementations and call these yourself when you see fit.
174
+
175
+ Another way assure proper cleanup of resources is to factor out GitPython into a
176
+ separate process which can be dropped periodically.
177
+
178
+ #### Windows support
179
+
180
+ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525).
181
+
182
+ ### RUNNING TESTS
183
+
184
+ _Important_: Right after cloning this repository, please be sure to have executed
185
+ the `./init-tests-after-clone.sh` script in the repository root. Otherwise
186
+ you will encounter test failures.
187
+
188
+ #### Install test dependencies
189
+
190
+ Ensure testing libraries are installed. This is taken care of already if you installed with:
191
+
192
+ ```sh
193
+ pip install -e ".[test]"
194
+ ```
195
+
196
+ If you had installed with a command like `pip install -e .` instead, you can still run
197
+ the above command to add the testing dependencies.
198
+
199
+ #### Test commands
200
+
201
+ To test, run:
202
+
203
+ ```sh
204
+ pytest
205
+ ```
206
+
207
+ To lint, and apply some linting fixes as well as automatic code formatting, run:
208
+
209
+ ```sh
210
+ pre-commit run --all-files
211
+ ```
212
+
213
+ This includes the linting and autoformatting done by Ruff, as well as some other checks.
214
+
215
+ To typecheck, run:
216
+
217
+ ```sh
218
+ mypy
219
+ ```
220
+
221
+ #### CI (and tox)
222
+
223
+ Style and formatting checks, and running tests on all the different supported Python versions, will be performed:
224
+
225
+ - Upon submitting a pull request.
226
+ - On each push, *if* you have a fork with GitHub Actions enabled.
227
+ - Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
228
+
229
+ #### Configuration files
230
+
231
+ Specific tools are all configured in the `./pyproject.toml` file:
232
+
233
+ - `pytest` (test runner)
234
+ - `coverage.py` (code coverage)
235
+ - `ruff` (linter and formatter)
236
+ - `mypy` (type checker)
237
+
238
+ Orchestration tools:
239
+
240
+ - Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file.
241
+ - Configuration for `tox` is in `./tox.ini`.
242
+ - Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`.
243
+
244
+ ### Contributions
245
+
246
+ Please have a look at the [contributions file][contributing].
247
+
248
+ ### INFRASTRUCTURE
249
+
250
+ - [User Documentation](http://gitpython.readthedocs.org)
251
+ - [Questions and Answers](http://stackexchange.com/filters/167317/gitpython)
252
+ - Please post on Stack Overflow and use the `gitpython` tag
253
+ - [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues)
254
+ - Post reproducible bugs and feature requests as a new issue.
255
+ Please be sure to provide the following information if posting bugs:
256
+ - GitPython version (e.g. `import git; git.__version__`)
257
+ - Python version (e.g. `python --version`)
258
+ - The encountered stack-trace, if applicable
259
+ - Enough information to allow reproducing the issue
260
+
261
+ ### How to make a new release
262
+
263
+ 1. Update/verify the **version** in the `VERSION` file.
264
+ 2. Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
265
+ 3. Commit everything.
266
+ 4. Run `git tag -s <version>` to tag the version in Git.
267
+ 5. _Optionally_ create and activate a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment). (Then the next step can install `build` and `twine`.)
268
+ 6. Run `make release`.
269
+ 7. Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog.
270
+
271
+ ### Projects using GitPython
272
+
273
+ - [PyDriller](https://github.com/ishepard/pydriller)
274
+ - [Kivy Designer](https://github.com/kivy/kivy-designer)
275
+ - [Prowl](https://github.com/nettitude/Prowl)
276
+ - [Python Taint](https://github.com/python-security/pyt)
277
+ - [Buster](https://github.com/axitkhurana/buster)
278
+ - [git-ftp](https://github.com/ezyang/git-ftp)
279
+ - [Git-Pandas](https://github.com/wdm0006/git-pandas)
280
+ - [PyGitUp](https://github.com/msiemens/PyGitUp)
281
+ - [PyJFuzz](https://github.com/mseclab/PyJFuzz)
282
+ - [Loki](https://github.com/Neo23x0/Loki)
283
+ - [Omniwallet](https://github.com/OmniLayer/omniwallet)
284
+ - [GitViper](https://github.com/BeayemX/GitViper)
285
+ - [Git Gud](https://github.com/bthayer2365/git-gud)
286
+
287
+ ### LICENSE
288
+
289
+ [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
290
+
291
+ One file exclusively used for fuzz testing is subject to [a separate license, detailed here](./fuzzing/README.md#license).
292
+ This file is not included in the wheel or sdist packages published by the maintainers of GitPython.
293
+
294
+ [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
295
+ [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/RECORD ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GitPython-3.1.44.dist-info/AUTHORS,sha256=tZ9LuyBks2V2HKTPK7kCmtd9Guu_LyU1oZHvU0NiAok,2334
2
+ GitPython-3.1.44.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
3
+ GitPython-3.1.44.dist-info/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
4
+ GitPython-3.1.44.dist-info/METADATA,sha256=0O_Fr2Y7A-DlPYhlbSxGjblBC2mWkw3USNUhyL80Ip8,13245
5
+ GitPython-3.1.44.dist-info/RECORD,,
6
+ GitPython-3.1.44.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ GitPython-3.1.44.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
8
+ GitPython-3.1.44.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
9
+ git/__init__.py,sha256=nkQImgv-bWdiZOFDjzN-gbt93FoRHD0nY6_t9LQxy4Y,8899
10
+ git/__pycache__/__init__.cpython-310.pyc,,
11
+ git/__pycache__/cmd.cpython-310.pyc,,
12
+ git/__pycache__/compat.cpython-310.pyc,,
13
+ git/__pycache__/config.cpython-310.pyc,,
14
+ git/__pycache__/db.cpython-310.pyc,,
15
+ git/__pycache__/diff.cpython-310.pyc,,
16
+ git/__pycache__/exc.cpython-310.pyc,,
17
+ git/__pycache__/remote.cpython-310.pyc,,
18
+ git/__pycache__/types.cpython-310.pyc,,
19
+ git/__pycache__/util.cpython-310.pyc,,
20
+ git/cmd.py,sha256=QwiaBy0mFbi9xjRKhRgUVK-_-K6xVdFqh9l0cxPqPSc,67724
21
+ git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
22
+ git/config.py,sha256=vTUlK6d8ORqFqjOv4Vbq_Hm-5mp-jOAt1dkq0IdzJ3U,34933
23
+ git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
24
+ git/diff.py,sha256=wmpMCIdMiVOqreGVPOGYyO4gFboGOAicyrvvI7PPjEg,27095
25
+ git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
26
+ git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
27
+ git/index/__pycache__/__init__.cpython-310.pyc,,
28
+ git/index/__pycache__/base.cpython-310.pyc,,
29
+ git/index/__pycache__/fun.cpython-310.pyc,,
30
+ git/index/__pycache__/typ.cpython-310.pyc,,
31
+ git/index/__pycache__/util.cpython-310.pyc,,
32
+ git/index/base.py,sha256=nDD7XVLNbgBKpJMrrTVyHBy6NVLWgDkk7oUw6ZOegPc,60808
33
+ git/index/fun.py,sha256=37cA3DBC9vpAnSVu5TGA072SnoF5XZOkOukExwlejHs,16736
34
+ git/index/typ.py,sha256=uuKNwitUw83FhVaLSwo4pY7PHDQudtZTLJrLGym4jcI,6570
35
+ git/index/util.py,sha256=fULi7GPG-MvprKrRCD5c15GNdzku_1E38We0d97WB3A,3659
36
+ git/objects/__init__.py,sha256=O6ZL_olX7e5-8iIbKviRPkVSJxN37WA-EC0q9d48U5Y,637
37
+ git/objects/__pycache__/__init__.cpython-310.pyc,,
38
+ git/objects/__pycache__/base.cpython-310.pyc,,
39
+ git/objects/__pycache__/blob.cpython-310.pyc,,
40
+ git/objects/__pycache__/commit.cpython-310.pyc,,
41
+ git/objects/__pycache__/fun.cpython-310.pyc,,
42
+ git/objects/__pycache__/tag.cpython-310.pyc,,
43
+ git/objects/__pycache__/tree.cpython-310.pyc,,
44
+ git/objects/__pycache__/util.cpython-310.pyc,,
45
+ git/objects/base.py,sha256=0dqNkSRVH0mk0-7ZKIkGBK7iNYrzLTVxwQFUd6CagsE,10277
46
+ git/objects/blob.py,sha256=zwwq0KfOMYeP5J2tW5CQatoLyeqFRlfkxP1Vwx1h07s,1215
47
+ git/objects/commit.py,sha256=GH1_83C9t7RGTukwozTHDgvxYQPRjTHhPDkXJyBbJyo,30553
48
+ git/objects/fun.py,sha256=B4jCqhAjm6Hl79GK58FPzW1H9K6Wc7Tx0rssyWmAcEE,8935
49
+ git/objects/submodule/__init__.py,sha256=6xySp767LVz3UylWgUalntS_nGXRuVzXxDuFAv_Wc2c,303
50
+ git/objects/submodule/__pycache__/__init__.cpython-310.pyc,,
51
+ git/objects/submodule/__pycache__/base.cpython-310.pyc,,
52
+ git/objects/submodule/__pycache__/root.cpython-310.pyc,,
53
+ git/objects/submodule/__pycache__/util.cpython-310.pyc,,
54
+ git/objects/submodule/base.py,sha256=MQ-2xV8JznGwy2hLQv1aeQNgAkhBhgc5tdtClFL3DmE,63901
55
+ git/objects/submodule/root.py,sha256=5eTtYNHasqdPq6q0oDCPr7IaO6uAHL3b4DxMoiO2LhE,20246
56
+ git/objects/submodule/util.py,sha256=sQqAYaiSJdFkZa9NlAuK_wTsMNiS-kkQnQjvIoJtc_o,3509
57
+ git/objects/tag.py,sha256=jAGESnpmTEv-dLakPzheT5ILZFFArcItnXYqfxfDrgc,4441
58
+ git/objects/tree.py,sha256=jJH888SHiP4dGzE-ra1yenQOyya_0C_MkHr06c1gHpM,13849
59
+ git/objects/util.py,sha256=Nlza4zLgdPmr_Yasyvvs6c1rKtW_wMxI6wDmQpQ3ufw,23846
60
+ git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
+ git/refs/__init__.py,sha256=DWlJNnsx-4jM_E-VycbP-FZUdn6iWhjnH_uZ_pZXBro,509
62
+ git/refs/__pycache__/__init__.cpython-310.pyc,,
63
+ git/refs/__pycache__/head.cpython-310.pyc,,
64
+ git/refs/__pycache__/log.cpython-310.pyc,,
65
+ git/refs/__pycache__/reference.cpython-310.pyc,,
66
+ git/refs/__pycache__/remote.cpython-310.pyc,,
67
+ git/refs/__pycache__/symbolic.cpython-310.pyc,,
68
+ git/refs/__pycache__/tag.cpython-310.pyc,,
69
+ git/refs/head.py,sha256=SGa3N301HfAi79X6UR5Mcg7mO9TnCH3Bk549kHlJVaQ,10513
70
+ git/refs/log.py,sha256=kXiuAgTo1DIuM_BfbDUk9gQ0YO-mutIMVdHv1_ES90o,12493
71
+ git/refs/reference.py,sha256=l6mhF4YLSEwtjz6b9PpOQH-fkng7EYWMaJhkjn-2jXA,5630
72
+ git/refs/remote.py,sha256=WwqV9T7BbYf3F_WZNUQivu9xktIIKGklCjDpwQrhD-A,2806
73
+ git/refs/symbolic.py,sha256=c8zOwaqzcg-J-rGrpuWdvh8zwMvSUqAHghd4vJoYG_s,34552
74
+ git/refs/tag.py,sha256=kgzV2vhpL4FD2TqHb0BJuMRAHgAvJF-TcoyWlaB-djQ,5010
75
+ git/remote.py,sha256=pYn9dAlz-QwvNMWXD1M57pMPQitthOM86qTRK_cpTqU,46786
76
+ git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
77
+ git/repo/__pycache__/__init__.cpython-310.pyc,,
78
+ git/repo/__pycache__/base.cpython-310.pyc,,
79
+ git/repo/__pycache__/fun.cpython-310.pyc,,
80
+ git/repo/base.py,sha256=0GU6nKNdT8SYjDI5Y5DeZ1zCEX3tHeq1VW2MSpne05g,59891
81
+ git/repo/fun.py,sha256=HSGC0-rqeKKx9fDg7JyQyMZgIwUWn-FnSZR_gRGpG-E,13573
82
+ git/types.py,sha256=MQzIDEOnoueXGsAJF_0MgUc_osH7Eu0Sw3DQofYzCVE,10272
83
+ git/util.py,sha256=2uAv34zZ_827-zJ3-D5ACrVH-4Q4EO_KLUTH23zi2AI,43770
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/REQUESTED ADDED
File without changes
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ git
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/RECORD ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cryptography-45.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2
+ cryptography-45.0.5.dist-info/METADATA,sha256=Dn2Ls7PhdM88VKgRTBFDskPTSNeGHm7SezAcdIPYdKc,5690
3
+ cryptography-45.0.5.dist-info/RECORD,,
4
+ cryptography-45.0.5.dist-info/WHEEL,sha256=VqNc6WOTGHZndw6FKXcyBqGvpOjDTrsbnnH79sUpcvY,107
5
+ cryptography-45.0.5.dist-info/licenses/LICENSE,sha256=Pgx8CRqUi4JTO6mP18u0BDLW8amsv4X1ki0vmak65rs,197
6
+ cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
7
+ cryptography-45.0.5.dist-info/licenses/LICENSE.BSD,sha256=YCxMdILeZHndLpeTzaJ15eY9dz2s0eymiSMqtwCPtPs,1532
8
+ cryptography/__about__.py,sha256=X8cr9wNd4PDeKHYUt6ppLOYb-V56WYSgLBuYKlivuBY,445
9
+ cryptography/__init__.py,sha256=QDuQ3te_14R9iGF-2Q5yEwD1FTJrCWqpnuhlMIJBsno,762
10
+ cryptography/__pycache__/__about__.cpython-310.pyc,,
11
+ cryptography/__pycache__/__init__.cpython-310.pyc,,
12
+ cryptography/__pycache__/exceptions.cpython-310.pyc,,
13
+ cryptography/__pycache__/fernet.cpython-310.pyc,,
14
+ cryptography/__pycache__/utils.cpython-310.pyc,,
15
+ cryptography/exceptions.py,sha256=835EWILc2fwxw-gyFMriciC2SqhViETB10LBSytnDIc,1087
16
+ cryptography/fernet.py,sha256=3Cvxkh0KJSbX8HbnCHu4wfCW7U0GgfUA3v_qQ8a8iWc,6963
17
+ cryptography/hazmat/__init__.py,sha256=5IwrLWrVp0AjEr_4FdWG_V057NSJGY_W4egNNsuct0g,455
18
+ cryptography/hazmat/__pycache__/__init__.cpython-310.pyc,,
19
+ cryptography/hazmat/__pycache__/_oid.cpython-310.pyc,,
20
+ cryptography/hazmat/_oid.py,sha256=7-aiQLWCYT7FDrQ0sUWBNYkN5ADEGTZwLQE_ZCksPxs,16795
21
+ cryptography/hazmat/backends/__init__.py,sha256=O5jvKFQdZnXhKeqJ-HtulaEL9Ni7mr1mDzZY5kHlYhI,361
22
+ cryptography/hazmat/backends/__pycache__/__init__.cpython-310.pyc,,
23
+ cryptography/hazmat/backends/openssl/__init__.py,sha256=p3jmJfnCag9iE5sdMrN6VvVEu55u46xaS_IjoI0SrmA,305
24
+ cryptography/hazmat/backends/openssl/__pycache__/__init__.cpython-310.pyc,,
25
+ cryptography/hazmat/backends/openssl/__pycache__/backend.cpython-310.pyc,,
26
+ cryptography/hazmat/backends/openssl/backend.py,sha256=ev-Prba_u1bCMk3ZWspkdOuLBbDlf_7wJnHK6xdYs7c,10329
27
+ cryptography/hazmat/bindings/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
28
+ cryptography/hazmat/bindings/__pycache__/__init__.cpython-310.pyc,,
29
+ cryptography/hazmat/bindings/_rust.abi3.so,sha256=BtscyLV0h1_Wy8zPS9g0OAG4KOJsnqLVG1w4ZqZnr5s,12446840
30
+ cryptography/hazmat/bindings/_rust/__init__.pyi,sha256=KhqLhXFPArPzzJ7DYO9Fl8FoXB_BagAd_r4Dm_Ze9Xo,1257
31
+ cryptography/hazmat/bindings/_rust/_openssl.pyi,sha256=mpNJLuYLbCVrd5i33FBTmWwL_55Dw7JPkSLlSX9Q7oI,230
32
+ cryptography/hazmat/bindings/_rust/asn1.pyi,sha256=BrGjC8J6nwuS-r3EVcdXJB8ndotfY9mbQYOfpbPG0HA,354
33
+ cryptography/hazmat/bindings/_rust/exceptions.pyi,sha256=exXr2xw_0pB1kk93cYbM3MohbzoUkjOms1ZMUi0uQZE,640
34
+ cryptography/hazmat/bindings/_rust/ocsp.pyi,sha256=VPVWuKHI9EMs09ZLRYAGvR0Iz0mCMmEzXAkgJHovpoM,4020
35
+ cryptography/hazmat/bindings/_rust/openssl/__init__.pyi,sha256=iOAMDyHoNwwCSZfZzuXDr64g4GpGUeDgEN-LjXqdrBM,1522
36
+ cryptography/hazmat/bindings/_rust/openssl/aead.pyi,sha256=4Nddw6-ynzIB3w2W86WvkGKTLlTDk_6F5l54RHCuy3E,2688
37
+ cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi,sha256=LhPzHWSXJq4grAJXn6zSvSSdV-aYIIscHDwIPlJGGPs,1315
38
+ cryptography/hazmat/bindings/_rust/openssl/cmac.pyi,sha256=nPH0X57RYpsAkRowVpjQiHE566ThUTx7YXrsadmrmHk,564
39
+ cryptography/hazmat/bindings/_rust/openssl/dh.pyi,sha256=Z3TC-G04-THtSdAOPLM1h2G7ml5bda1ElZUcn5wpuhk,1564
40
+ cryptography/hazmat/bindings/_rust/openssl/dsa.pyi,sha256=qBtkgj2albt2qFcnZ9UDrhzoNhCVO7HTby5VSf1EXMI,1299
41
+ cryptography/hazmat/bindings/_rust/openssl/ec.pyi,sha256=zJy0pRa5n-_p2dm45PxECB_-B6SVZyNKfjxFDpPqT38,1691
42
+ cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi,sha256=VXfXd5G6hUivg399R1DYdmW3eTb0EebzDTqjRC2gaRw,532
43
+ cryptography/hazmat/bindings/_rust/openssl/ed448.pyi,sha256=Yx49lqdnjsD7bxiDV1kcaMrDktug5evi5a6zerMiy2s,514
44
+ cryptography/hazmat/bindings/_rust/openssl/hashes.pyi,sha256=OWZvBx7xfo_HJl41Nc--DugVyCVPIprZ3HlOPTSWH9g,984
45
+ cryptography/hazmat/bindings/_rust/openssl/hmac.pyi,sha256=BXZn7NDjL3JAbYW0SQ8pg1iyC5DbQXVhUAiwsi8DFR8,702
46
+ cryptography/hazmat/bindings/_rust/openssl/kdf.pyi,sha256=4FGpmCR2H8SVGoe4SUX7h5wzhyvuRi8hnzKKfvnJJrw,1379
47
+ cryptography/hazmat/bindings/_rust/openssl/keys.pyi,sha256=teIt8M6ZEMJrn4s3W0UnW0DZ-30Jd68WnSsKKG124l0,912
48
+ cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi,sha256=_SW9NtQ5FDlAbdclFtWpT4lGmxKIKHpN-4j8J2BzYfQ,585
49
+ cryptography/hazmat/bindings/_rust/openssl/rsa.pyi,sha256=2OQCNSXkxgc-3uw1xiCCloIQTV6p9_kK79Yu0rhZgPc,1364
50
+ cryptography/hazmat/bindings/_rust/openssl/x25519.pyi,sha256=ewn4GpQyb7zPwE-ni7GtyQgMC0A1mLuqYsSyqv6nI_s,523
51
+ cryptography/hazmat/bindings/_rust/openssl/x448.pyi,sha256=juTZTmli8jO_5Vcufg-vHvx_tCyezmSLIh_9PU3TczI,505
52
+ cryptography/hazmat/bindings/_rust/pkcs12.pyi,sha256=vEEd5wDiZvb8ZGFaziLCaWLzAwoG_tvPUxLQw5_uOl8,1605
53
+ cryptography/hazmat/bindings/_rust/pkcs7.pyi,sha256=txGBJijqZshEcqra6byPNbnisIdlxzOSIHP2hl9arPs,1601
54
+ cryptography/hazmat/bindings/_rust/test_support.pyi,sha256=PPhld-WkO743iXFPebeG0LtgK0aTzGdjcIsay1Gm5GE,757
55
+ cryptography/hazmat/bindings/_rust/x509.pyi,sha256=WWFr7RV3n1eFYbLpmM_xwXGKYZalyYi_V07p6KLOHX8,10189
56
+ cryptography/hazmat/bindings/openssl/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
57
+ cryptography/hazmat/bindings/openssl/__pycache__/__init__.cpython-310.pyc,,
58
+ cryptography/hazmat/bindings/openssl/__pycache__/_conditional.cpython-310.pyc,,
59
+ cryptography/hazmat/bindings/openssl/__pycache__/binding.cpython-310.pyc,,
60
+ cryptography/hazmat/bindings/openssl/_conditional.py,sha256=eKiDLdRSXTGA6mQ8jj5S2OXEB_sod6iL7cwed-yOuUw,5340
61
+ cryptography/hazmat/bindings/openssl/binding.py,sha256=hAmRXYzQlpIAP_jj2tl-SIBJBxwtclLlr_3aIL-RkR0,4072
62
+ cryptography/hazmat/decrepit/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
63
+ cryptography/hazmat/decrepit/__pycache__/__init__.cpython-310.pyc,,
64
+ cryptography/hazmat/decrepit/ciphers/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
65
+ cryptography/hazmat/decrepit/ciphers/__pycache__/__init__.cpython-310.pyc,,
66
+ cryptography/hazmat/decrepit/ciphers/__pycache__/algorithms.cpython-310.pyc,,
67
+ cryptography/hazmat/decrepit/ciphers/algorithms.py,sha256=YrKgHS4MfwWaMmPBYRymRRlC0phwWp9ycICFezeJPGk,2595
68
+ cryptography/hazmat/primitives/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
69
+ cryptography/hazmat/primitives/__pycache__/__init__.cpython-310.pyc,,
70
+ cryptography/hazmat/primitives/__pycache__/_asymmetric.cpython-310.pyc,,
71
+ cryptography/hazmat/primitives/__pycache__/_cipheralgorithm.cpython-310.pyc,,
72
+ cryptography/hazmat/primitives/__pycache__/_serialization.cpython-310.pyc,,
73
+ cryptography/hazmat/primitives/__pycache__/cmac.cpython-310.pyc,,
74
+ cryptography/hazmat/primitives/__pycache__/constant_time.cpython-310.pyc,,
75
+ cryptography/hazmat/primitives/__pycache__/hashes.cpython-310.pyc,,
76
+ cryptography/hazmat/primitives/__pycache__/hmac.cpython-310.pyc,,
77
+ cryptography/hazmat/primitives/__pycache__/keywrap.cpython-310.pyc,,
78
+ cryptography/hazmat/primitives/__pycache__/padding.cpython-310.pyc,,
79
+ cryptography/hazmat/primitives/__pycache__/poly1305.cpython-310.pyc,,
80
+ cryptography/hazmat/primitives/_asymmetric.py,sha256=RhgcouUB6HTiFDBrR1LxqkMjpUxIiNvQ1r_zJjRG6qQ,532
81
+ cryptography/hazmat/primitives/_cipheralgorithm.py,sha256=Eh3i7lwedHfi0eLSsH93PZxQKzY9I6lkK67vL4V5tOc,1522
82
+ cryptography/hazmat/primitives/_serialization.py,sha256=chgPCSF2jxI2Cr5gB-qbWXOvOfupBh4CARS0KAhv9AM,5123
83
+ cryptography/hazmat/primitives/asymmetric/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
84
+ cryptography/hazmat/primitives/asymmetric/__pycache__/__init__.cpython-310.pyc,,
85
+ cryptography/hazmat/primitives/asymmetric/__pycache__/dh.cpython-310.pyc,,
86
+ cryptography/hazmat/primitives/asymmetric/__pycache__/dsa.cpython-310.pyc,,
87
+ cryptography/hazmat/primitives/asymmetric/__pycache__/ec.cpython-310.pyc,,
88
+ cryptography/hazmat/primitives/asymmetric/__pycache__/ed25519.cpython-310.pyc,,
89
+ cryptography/hazmat/primitives/asymmetric/__pycache__/ed448.cpython-310.pyc,,
90
+ cryptography/hazmat/primitives/asymmetric/__pycache__/padding.cpython-310.pyc,,
91
+ cryptography/hazmat/primitives/asymmetric/__pycache__/rsa.cpython-310.pyc,,
92
+ cryptography/hazmat/primitives/asymmetric/__pycache__/types.cpython-310.pyc,,
93
+ cryptography/hazmat/primitives/asymmetric/__pycache__/utils.cpython-310.pyc,,
94
+ cryptography/hazmat/primitives/asymmetric/__pycache__/x25519.cpython-310.pyc,,
95
+ cryptography/hazmat/primitives/asymmetric/__pycache__/x448.cpython-310.pyc,,
96
+ cryptography/hazmat/primitives/asymmetric/dh.py,sha256=0v_vEFFz5pQ1QG-FkWDyvgv7IfuVZSH5Q6LyFI5A8rg,3645
97
+ cryptography/hazmat/primitives/asymmetric/dsa.py,sha256=Ld_bbbqQFz12dObHxIkzEQzX0SWWP41RLSWkYSaKhqE,4213
98
+ cryptography/hazmat/primitives/asymmetric/ec.py,sha256=Vf5ig2PcS3PVnsb5N49Kx1uIkFBJyhg4BWXThDz5cug,12999
99
+ cryptography/hazmat/primitives/asymmetric/ed25519.py,sha256=jZW5cs472wXXV3eB0sE1b8w64gdazwwU0_MT5UOTiXs,3700
100
+ cryptography/hazmat/primitives/asymmetric/ed448.py,sha256=yAetgn2f2JYf0BO8MapGzXeThsvSMG5LmUCrxVOidAA,3729
101
+ cryptography/hazmat/primitives/asymmetric/padding.py,sha256=eZcvUqVLbe3u48SunLdeniaPlV4-k6pwBl67OW4jSy8,2885
102
+ cryptography/hazmat/primitives/asymmetric/rsa.py,sha256=kegiZAGeb6yJISjpxpaAEpM3wGdfhJSU9RgZpVwKYwk,7967
103
+ cryptography/hazmat/primitives/asymmetric/types.py,sha256=LnsOJym-wmPUJ7Knu_7bCNU3kIiELCd6krOaW_JU08I,2996
104
+ cryptography/hazmat/primitives/asymmetric/utils.py,sha256=DPTs6T4F-UhwzFQTh-1fSEpQzazH2jf2xpIro3ItF4o,790
105
+ cryptography/hazmat/primitives/asymmetric/x25519.py,sha256=_4nQeZ3yJ3Lg0RpXnaqA-1yt6vbx1F-wzLcaZHwSpeE,3613
106
+ cryptography/hazmat/primitives/asymmetric/x448.py,sha256=WKBLtuVfJqiBRro654fGaQAlvsKbqbNkK7c4A_ZCdV0,3642
107
+ cryptography/hazmat/primitives/ciphers/__init__.py,sha256=eyEXmjk6_CZXaOPYDr7vAYGXr29QvzgWL2-4CSolLFs,680
108
+ cryptography/hazmat/primitives/ciphers/__pycache__/__init__.cpython-310.pyc,,
109
+ cryptography/hazmat/primitives/ciphers/__pycache__/aead.cpython-310.pyc,,
110
+ cryptography/hazmat/primitives/ciphers/__pycache__/algorithms.cpython-310.pyc,,
111
+ cryptography/hazmat/primitives/ciphers/__pycache__/base.cpython-310.pyc,,
112
+ cryptography/hazmat/primitives/ciphers/__pycache__/modes.cpython-310.pyc,,
113
+ cryptography/hazmat/primitives/ciphers/aead.py,sha256=Fzlyx7w8KYQakzDp1zWgJnIr62zgZrgVh1u2h4exB54,634
114
+ cryptography/hazmat/primitives/ciphers/algorithms.py,sha256=S04j4NdFCBDt5KLIvbYohohRO--MCWhTia_lkVt8xD8,4542
115
+ cryptography/hazmat/primitives/ciphers/base.py,sha256=aBC7HHBBoixebmparVr0UlODs3VD0A7B6oz_AaRjDv8,4253
116
+ cryptography/hazmat/primitives/ciphers/modes.py,sha256=20stpwhDtbAvpH0SMf9EDHIciwmTF-JMBUOZ9bU8WiQ,8318
117
+ cryptography/hazmat/primitives/cmac.py,sha256=sz_s6H_cYnOvx-VNWdIKhRhe3Ymp8z8J0D3CBqOX3gg,338
118
+ cryptography/hazmat/primitives/constant_time.py,sha256=xdunWT0nf8OvKdcqUhhlFKayGp4_PgVJRU2W1wLSr_A,422
119
+ cryptography/hazmat/primitives/hashes.py,sha256=M8BrlKB3U6DEtHvWTV5VRjpteHv1kS3Zxm_Bsk04cr8,5184
120
+ cryptography/hazmat/primitives/hmac.py,sha256=RpB3z9z5skirCQrm7zQbtnp9pLMnAjrlTUvKqF5aDDc,423
121
+ cryptography/hazmat/primitives/kdf/__init__.py,sha256=4XibZnrYq4hh5xBjWiIXzaYW6FKx8hPbVaa_cB9zS64,750
122
+ cryptography/hazmat/primitives/kdf/__pycache__/__init__.cpython-310.pyc,,
123
+ cryptography/hazmat/primitives/kdf/__pycache__/argon2.cpython-310.pyc,,
124
+ cryptography/hazmat/primitives/kdf/__pycache__/concatkdf.cpython-310.pyc,,
125
+ cryptography/hazmat/primitives/kdf/__pycache__/hkdf.cpython-310.pyc,,
126
+ cryptography/hazmat/primitives/kdf/__pycache__/kbkdf.cpython-310.pyc,,
127
+ cryptography/hazmat/primitives/kdf/__pycache__/pbkdf2.cpython-310.pyc,,
128
+ cryptography/hazmat/primitives/kdf/__pycache__/scrypt.cpython-310.pyc,,
129
+ cryptography/hazmat/primitives/kdf/__pycache__/x963kdf.cpython-310.pyc,,
130
+ cryptography/hazmat/primitives/kdf/argon2.py,sha256=UFDNXG0v-rw3DqAQTB1UQAsQC2M5Ejg0k_6OCyhLKus,460
131
+ cryptography/hazmat/primitives/kdf/concatkdf.py,sha256=Ua8KoLXXnzgsrAUmHpyKymaPt8aPRP0EHEaBz7QCQ9I,3737
132
+ cryptography/hazmat/primitives/kdf/hkdf.py,sha256=2HlP_huUzGAy_FZWOn8L9qP9B5ke7XKmbiER7_2l__Q,3043
133
+ cryptography/hazmat/primitives/kdf/kbkdf.py,sha256=tGH5dQGEVaKYMEbgeqlaSPpqhlDMoF0xwCxk2vuhcTc,9211
134
+ cryptography/hazmat/primitives/kdf/pbkdf2.py,sha256=1WIwhELR0w8ztTpTu8BrFiYWmK3hUfJq08I79TxwieE,1957
135
+ cryptography/hazmat/primitives/kdf/scrypt.py,sha256=XyWUdUUmhuI9V6TqAPOvujCSMGv1XQdg0a21IWCmO-U,590
136
+ cryptography/hazmat/primitives/kdf/x963kdf.py,sha256=zLTcF665QFvXX2f8TS7fmBZTteXpFjKahzfjjQcCJyw,1999
137
+ cryptography/hazmat/primitives/keywrap.py,sha256=XV4Pj2fqSeD-RqZVvY2cA3j5_7RwJSFygYuLfk2ujCo,5650
138
+ cryptography/hazmat/primitives/padding.py,sha256=QT-U-NvV2eQGO1wVPbDiNGNSc9keRDS-ig5cQOrLz0E,1865
139
+ cryptography/hazmat/primitives/poly1305.py,sha256=P5EPQV-RB_FJPahpg01u0Ts4S_PnAmsroxIGXbGeRRo,355
140
+ cryptography/hazmat/primitives/serialization/__init__.py,sha256=Q7uTgDlt7n3WfsMT6jYwutC6DIg_7SEeoAm1GHZ5B5E,1705
141
+ cryptography/hazmat/primitives/serialization/__pycache__/__init__.cpython-310.pyc,,
142
+ cryptography/hazmat/primitives/serialization/__pycache__/base.cpython-310.pyc,,
143
+ cryptography/hazmat/primitives/serialization/__pycache__/pkcs12.cpython-310.pyc,,
144
+ cryptography/hazmat/primitives/serialization/__pycache__/pkcs7.cpython-310.pyc,,
145
+ cryptography/hazmat/primitives/serialization/__pycache__/ssh.cpython-310.pyc,,
146
+ cryptography/hazmat/primitives/serialization/base.py,sha256=ikq5MJIwp_oUnjiaBco_PmQwOTYuGi-XkYUYHKy8Vo0,615
147
+ cryptography/hazmat/primitives/serialization/pkcs12.py,sha256=mS9cFNG4afzvseoc5e1MWoY2VskfL8N8Y_OFjl67luY,5104
148
+ cryptography/hazmat/primitives/serialization/pkcs7.py,sha256=5OR_Tkysxaprn4FegvJIfbep9rJ9wok6FLWvWwQ5-Mg,13943
149
+ cryptography/hazmat/primitives/serialization/ssh.py,sha256=CeDSD_KmtcpQDSUg8QY3jruZDOKjjWBKcUByDOGQXnY,53693
150
+ cryptography/hazmat/primitives/twofactor/__init__.py,sha256=tmMZGB-g4IU1r7lIFqASU019zr0uPp_wEBYcwdDCKCA,258
151
+ cryptography/hazmat/primitives/twofactor/__pycache__/__init__.cpython-310.pyc,,
152
+ cryptography/hazmat/primitives/twofactor/__pycache__/hotp.cpython-310.pyc,,
153
+ cryptography/hazmat/primitives/twofactor/__pycache__/totp.cpython-310.pyc,,
154
+ cryptography/hazmat/primitives/twofactor/hotp.py,sha256=ivZo5BrcCGWLsqql4nZV0XXCjyGPi_iHfDFltGlOJwk,3256
155
+ cryptography/hazmat/primitives/twofactor/totp.py,sha256=m5LPpRL00kp4zY8gTjr55Hfz9aMlPS53kHmVkSQCmdY,1652
156
+ cryptography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
+ cryptography/utils.py,sha256=r90VtamCjiba37omFqp2aa--eEQVXiQIMrvWeLT-BY4,4397
158
+ cryptography/x509/__init__.py,sha256=xloN0swseNx-m2WFZmCA17gOoxQWqeU82UVjEdJBePQ,8257
159
+ cryptography/x509/__pycache__/__init__.cpython-310.pyc,,
160
+ cryptography/x509/__pycache__/base.cpython-310.pyc,,
161
+ cryptography/x509/__pycache__/certificate_transparency.cpython-310.pyc,,
162
+ cryptography/x509/__pycache__/extensions.cpython-310.pyc,,
163
+ cryptography/x509/__pycache__/general_name.cpython-310.pyc,,
164
+ cryptography/x509/__pycache__/name.cpython-310.pyc,,
165
+ cryptography/x509/__pycache__/ocsp.cpython-310.pyc,,
166
+ cryptography/x509/__pycache__/oid.cpython-310.pyc,,
167
+ cryptography/x509/__pycache__/verification.cpython-310.pyc,,
168
+ cryptography/x509/base.py,sha256=OrmTw3y8B6AE_nGXQPN8x9kq-d7rDWeH13gCq6T6D6U,27997
169
+ cryptography/x509/certificate_transparency.py,sha256=JqoOIDhlwInrYMFW6IFn77WJ0viF-PB_rlZV3vs9MYc,797
170
+ cryptography/x509/extensions.py,sha256=QxYrqR6SF1qzR9ZraP8wDiIczlEVlAFuwDRVcltB6Tk,77724
171
+ cryptography/x509/general_name.py,sha256=sP_rV11Qlpsk4x3XXGJY_Mv0Q_s9dtjeLckHsjpLQoQ,7836
172
+ cryptography/x509/name.py,sha256=oi0CqY_B72CEJgNXy9XkJya26QWXfwOeGJBYfXjRYvI,15121
173
+ cryptography/x509/ocsp.py,sha256=Yey6NdFV1MPjop24Mj_VenjEpg3kUaMopSWOK0AbeBs,12699
174
+ cryptography/x509/oid.py,sha256=BUzgXXGVWilkBkdKPTm9R4qElE9gAGHgdYPMZAp7PJo,931
175
+ cryptography/x509/verification.py,sha256=gR2C2c-XZQtblZhT5T5vjSKOtCb74ef2alPVmEcwFlM,958
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.8.6)
3
+ Root-Is-Purelib: false
4
+ Tag: cp37-abi3-manylinux_2_34_x86_64
5
+
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ https://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ https://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
venv/lib/python3.10/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.44'
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.10/site-packages/git/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (5.29 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/cmd.cpython-310.pyc ADDED
Binary file (47.9 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/compat.cpython-310.pyc ADDED
Binary file (3.27 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/config.cpython-310.pyc ADDED
Binary file (27.4 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/db.cpython-310.pyc ADDED
Binary file (2.61 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/diff.cpython-310.pyc ADDED
Binary file (17.8 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/exc.cpython-310.pyc ADDED
Binary file (7.72 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/remote.cpython-310.pyc ADDED
Binary file (35.1 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/types.cpython-310.pyc ADDED
Binary file (4.83 kB). View file
 
venv/lib/python3.10/site-packages/git/__pycache__/util.cpython-310.pyc ADDED
Binary file (39.3 kB). View file
 
venv/lib/python3.10/site-packages/git/cmd.py ADDED
@@ -0,0 +1,1724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from git.types import Literal, PathLike, TBD
64
+
65
+ if TYPE_CHECKING:
66
+ from git.diff import DiffIndex
67
+ from git.repo.base import Repo
68
+
69
+ # ---------------------------------------------------------------------------------
70
+
71
+ execute_kwargs = {
72
+ "istream",
73
+ "with_extended_output",
74
+ "with_exceptions",
75
+ "as_process",
76
+ "output_stream",
77
+ "stdout_as_string",
78
+ "kill_after_timeout",
79
+ "with_stdout",
80
+ "universal_newlines",
81
+ "shell",
82
+ "env",
83
+ "max_chunk_size",
84
+ "strip_newline_in_stdout",
85
+ }
86
+
87
+ _logger = logging.getLogger(__name__)
88
+
89
+
90
+ # ==============================================================================
91
+ ## @name Utilities
92
+ # ------------------------------------------------------------------------------
93
+ # Documentation
94
+ ## @{
95
+
96
+
97
+ def handle_process_output(
98
+ process: "Git.AutoInterrupt" | Popen,
99
+ stdout_handler: Union[
100
+ None,
101
+ Callable[[AnyStr], None],
102
+ Callable[[List[AnyStr]], None],
103
+ Callable[[bytes, "Repo", "DiffIndex"], None],
104
+ ],
105
+ stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
106
+ finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
107
+ decode_streams: bool = True,
108
+ kill_after_timeout: Union[None, float] = None,
109
+ ) -> None:
110
+ R"""Register for notifications to learn that process output is ready to read, and
111
+ dispatch lines to the respective line handlers.
112
+
113
+ This function returns once the finalizer returns.
114
+
115
+ :param process:
116
+ :class:`subprocess.Popen` instance.
117
+
118
+ :param stdout_handler:
119
+ f(stdout_line_string), or ``None``.
120
+
121
+ :param stderr_handler:
122
+ f(stderr_line_string), or ``None``.
123
+
124
+ :param finalizer:
125
+ f(proc) - wait for proc to finish.
126
+
127
+ :param decode_streams:
128
+ Assume stdout/stderr streams are binary and decode them before pushing their
129
+ contents to handlers.
130
+
131
+ This defaults to ``True``. Set it to ``False`` if:
132
+
133
+ - ``universal_newlines == True``, as then streams are in text mode, or
134
+ - decoding must happen later, such as for :class:`~git.diff.Diff`\s.
135
+
136
+ :param kill_after_timeout:
137
+ :class:`float` or ``None``, Default = ``None``
138
+
139
+ To specify a timeout in seconds for the git command, after which the process
140
+ should be killed.
141
+ """
142
+
143
+ # Use 2 "pump" threads and wait for both to finish.
144
+ def pump_stream(
145
+ cmdline: List[str],
146
+ name: str,
147
+ stream: Union[BinaryIO, TextIO],
148
+ is_decode: bool,
149
+ handler: Union[None, Callable[[Union[bytes, str]], None]],
150
+ ) -> None:
151
+ try:
152
+ for line in stream:
153
+ if handler:
154
+ if is_decode:
155
+ assert isinstance(line, bytes)
156
+ line_str = line.decode(defenc)
157
+ handler(line_str)
158
+ else:
159
+ handler(line)
160
+
161
+ except Exception as ex:
162
+ _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
163
+ if "I/O operation on closed file" not in str(ex):
164
+ # Only reraise if the error was not due to the stream closing.
165
+ raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
166
+ finally:
167
+ stream.close()
168
+
169
+ if hasattr(process, "proc"):
170
+ process = cast("Git.AutoInterrupt", process)
171
+ cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "")
172
+ p_stdout = process.proc.stdout if process.proc else None
173
+ p_stderr = process.proc.stderr if process.proc else None
174
+ else:
175
+ process = cast(Popen, process) # type: ignore[redundant-cast]
176
+ cmdline = getattr(process, "args", "")
177
+ p_stdout = process.stdout
178
+ p_stderr = process.stderr
179
+
180
+ if not isinstance(cmdline, (tuple, list)):
181
+ cmdline = cmdline.split()
182
+
183
+ pumps: List[Tuple[str, IO, Callable[..., None] | None]] = []
184
+ if p_stdout:
185
+ pumps.append(("stdout", p_stdout, stdout_handler))
186
+ if p_stderr:
187
+ pumps.append(("stderr", p_stderr, stderr_handler))
188
+
189
+ threads: List[threading.Thread] = []
190
+
191
+ for name, stream, handler in pumps:
192
+ t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler))
193
+ t.daemon = True
194
+ t.start()
195
+ threads.append(t)
196
+
197
+ # FIXME: Why join? Will block if stdin needs feeding...
198
+ for t in threads:
199
+ t.join(timeout=kill_after_timeout)
200
+ if t.is_alive():
201
+ if isinstance(process, Git.AutoInterrupt):
202
+ process._terminate()
203
+ else: # Don't want to deal with the other case.
204
+ raise RuntimeError(
205
+ "Thread join() timed out in cmd.handle_process_output()."
206
+ f" kill_after_timeout={kill_after_timeout} seconds"
207
+ )
208
+ if stderr_handler:
209
+ error_str: Union[str, bytes] = (
210
+ "error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds"
211
+ )
212
+ if not decode_streams and isinstance(p_stderr, BinaryIO):
213
+ # Assume stderr_handler needs binary input.
214
+ error_str = cast(str, error_str)
215
+ error_str = error_str.encode()
216
+ # We ignore typing on the next line because mypy does not like the way
217
+ # we inferred that stderr takes str or bytes.
218
+ stderr_handler(error_str) # type: ignore[arg-type]
219
+
220
+ if finalizer:
221
+ finalizer(process)
222
+
223
+
224
+ safer_popen: Callable[..., Popen]
225
+
226
+ if sys.platform == "win32":
227
+
228
+ def _safer_popen_windows(
229
+ command: Union[str, Sequence[Any]],
230
+ *,
231
+ shell: bool = False,
232
+ env: Optional[Mapping[str, str]] = None,
233
+ **kwargs: Any,
234
+ ) -> Popen:
235
+ """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the
236
+ search.
237
+
238
+ This avoids an untrusted search path condition where a file like ``git.exe`` in
239
+ a malicious repository would be run when GitPython operates on the repository.
240
+ The process using GitPython may have an untrusted repository's working tree as
241
+ its current working directory. Some operations may temporarily change to that
242
+ directory before running a subprocess. In addition, while by default GitPython
243
+ does not run external commands with a shell, it can be made to do so, in which
244
+ case the CWD of the subprocess, which GitPython usually sets to a repository
245
+ working tree, can itself be searched automatically by the shell. This wrapper
246
+ covers all those cases.
247
+
248
+ :note:
249
+ This currently works by setting the
250
+ :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during
251
+ subprocess creation. It also takes care of passing Windows-specific process
252
+ creation flags, but that is unrelated to path search.
253
+
254
+ :note:
255
+ The current implementation contains a race condition on :attr:`os.environ`.
256
+ GitPython isn't thread-safe, but a program using it on one thread should
257
+ ideally be able to mutate :attr:`os.environ` on another, without
258
+ unpredictable results. See comments in:
259
+ https://github.com/gitpython-developers/GitPython/pull/1650
260
+ """
261
+ # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards.
262
+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
263
+ # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
264
+ creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
265
+
266
+ # When using a shell, the shell is the direct subprocess, so the variable must
267
+ # be set in its environment, to affect its search behavior.
268
+ if shell:
269
+ # The original may be immutable, or the caller may reuse it. Mutate a copy.
270
+ env = {} if env is None else dict(env)
271
+ env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be an value.
272
+
273
+ # When not using a shell, the current process does the search in a
274
+ # CreateProcessW API call, so the variable must be set in our environment. With
275
+ # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
276
+ # is patched. In Python versions where it is unpatched, and in the rare case the
277
+ # ComSpec environment variable is unset, the search for the shell itself is
278
+ # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
279
+ # is simpler and protects against that. (As above, the "1" can be any value.)
280
+ with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
281
+ return Popen(
282
+ command,
283
+ shell=shell,
284
+ env=env,
285
+ creationflags=creationflags,
286
+ **kwargs,
287
+ )
288
+
289
+ safer_popen = _safer_popen_windows
290
+ else:
291
+ safer_popen = Popen
292
+
293
+
294
+ def dashify(string: str) -> str:
295
+ return string.replace("_", "-")
296
+
297
+
298
+ def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]:
299
+ return {s: getattr(self, s) for s in self.__slots__ if s not in exclude}
300
+
301
+
302
+ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None:
303
+ for k, v in d.items():
304
+ setattr(self, k, v)
305
+ for k in excluded:
306
+ setattr(self, k, None)
307
+
308
+
309
+ ## -- End Utilities -- @}
310
+
311
+ _USE_SHELL_DEFAULT_MESSAGE = (
312
+ "Git.USE_SHELL is deprecated, because only its default value of False is safe. "
313
+ "It will be removed in a future release."
314
+ )
315
+
316
+ _USE_SHELL_DANGER_MESSAGE = (
317
+ "Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
318
+ "shell syntax cannot usually be accounted for. This can result in a command "
319
+ "injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
320
+ "and will be removed in a future release."
321
+ )
322
+
323
+
324
+ def _warn_use_shell(extra_danger: bool) -> None:
325
+ warnings.warn(
326
+ _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
327
+ DeprecationWarning,
328
+ stacklevel=3,
329
+ )
330
+
331
+
332
+ class _GitMeta(type):
333
+ """Metaclass for :class:`Git`.
334
+
335
+ This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
336
+ """
337
+
338
+ def __getattribute(cls, name: str) -> Any:
339
+ if name == "USE_SHELL":
340
+ _warn_use_shell(False)
341
+ return super().__getattribute__(name)
342
+
343
+ def __setattr(cls, name: str, value: Any) -> Any:
344
+ if name == "USE_SHELL":
345
+ _warn_use_shell(value)
346
+ super().__setattr__(name, value)
347
+
348
+ if not TYPE_CHECKING:
349
+ # To preserve static checking for undefined/misspelled attributes while letting
350
+ # the methods' bodies be type-checked, these are defined as non-special methods,
351
+ # then bound to special names out of view of static type checkers. (The original
352
+ # names invoke name mangling (leading "__") to avoid confusion in other scopes.)
353
+ __getattribute__ = __getattribute
354
+ __setattr__ = __setattr
355
+
356
+
357
+ GitMeta = _GitMeta
358
+ """Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
359
+
360
+ Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
361
+ custom metaclass is not documented and may change at any time. This statically checkable
362
+ metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
363
+ used. Code that benefits from it is likely to be remain brittle even if it is used.
364
+
365
+ In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
366
+ callable attributes representing git subcommands, it rarely makes sense to inherit from
367
+ :class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
368
+ to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
369
+ as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
370
+ might have to be introduced to solve a metaclass conflict, is not recommended.
371
+
372
+ :note:
373
+ The correct static type of the :class:`Git` class itself, and any subclasses, is
374
+ ``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
375
+
376
+ :class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
377
+ intended or otherwise possible to use. This alias is truly only for very rare and
378
+ inherently precarious situations where it is necessary to deal with the metaclass
379
+ explicitly.
380
+ """
381
+
382
+
383
+ class Git(metaclass=_GitMeta):
384
+ """The Git class manages communication with the Git binary.
385
+
386
+ It provides a convenient interface to calling the Git binary, such as in::
387
+
388
+ g = Git( git_dir )
389
+ g.init() # calls 'git init' program
390
+ rval = g.ls_files() # calls 'git ls-files' program
391
+
392
+ Debugging:
393
+
394
+ * Set the :envvar:`GIT_PYTHON_TRACE` environment variable to print each invocation
395
+ of the command to stdout.
396
+ * Set its value to ``full`` to see details about the returned values.
397
+ """
398
+
399
+ __slots__ = (
400
+ "_working_dir",
401
+ "cat_file_all",
402
+ "cat_file_header",
403
+ "_version_info",
404
+ "_version_info_token",
405
+ "_git_options",
406
+ "_persistent_git_options",
407
+ "_environment",
408
+ )
409
+
410
+ _excluded_ = (
411
+ "cat_file_all",
412
+ "cat_file_header",
413
+ "_version_info",
414
+ "_version_info_token",
415
+ )
416
+
417
+ re_unsafe_protocol = re.compile(r"(.+)::.+")
418
+
419
+ def __getstate__(self) -> Dict[str, Any]:
420
+ return slots_to_dict(self, exclude=self._excluded_)
421
+
422
+ def __setstate__(self, d: Dict[str, Any]) -> None:
423
+ dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_)
424
+
425
+ # CONFIGURATION
426
+
427
+ git_exec_name = "git"
428
+ """Default git command that should work on Linux, Windows, and other systems."""
429
+
430
+ GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
431
+ """Enables debugging of GitPython's git commands."""
432
+
433
+ USE_SHELL: bool = False
434
+ """Deprecated. If set to ``True``, a shell will be used when executing git commands.
435
+
436
+ Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
437
+ functions should be updated to use the default value of ``False`` instead. ``True``
438
+ is unsafe unless the effect of syntax treated specially by the shell is fully
439
+ considered and accounted for, which is not possible under most circumstances. As
440
+ detailed below, it is also no longer needed, even where it had been in the past.
441
+
442
+ It is in many if not most cases a command injection vulnerability for an application
443
+ to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
444
+ fragment of text to make its way into any part of any argument to any git command
445
+ (including paths, branch names, etc.) can cause the shell to read and write
446
+ arbitrary files and execute arbitrary commands. Innocent input may also accidentally
447
+ contain special shell syntax, leading to inadvertent malfunctions.
448
+
449
+ In addition, how a value of ``True`` interacts with some aspects of GitPython's
450
+ operation is not precisely specified and may change without warning, even before
451
+ GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
452
+
453
+ * Whether or how GitPython automatically customizes the shell environment.
454
+
455
+ * Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
456
+ separate arguments even when ``shell=True``), this can be used with any GitPython
457
+ functionality other than direct calls to the :meth:`execute` method.
458
+
459
+ * Whether any GitPython feature that runs git commands ever attempts to partially
460
+ sanitize data a shell may treat specially. Currently this is not done.
461
+
462
+ Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
463
+ in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
464
+ GitPython solves that problem more robustly and safely by using the
465
+ ``CREATE_NO_WINDOW`` process creation flag on Windows.
466
+
467
+ Because Windows path search differs subtly based on whether a shell is used, in rare
468
+ cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
469
+ such as a batch file, from being found. To fix this, set the command name or full
470
+ path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
471
+ full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
472
+
473
+ Further reading:
474
+
475
+ * :meth:`Git.execute` (on the ``shell`` parameter).
476
+ * https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
477
+ * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
478
+ * https://github.com/python/cpython/issues/91558#issuecomment-1100942950
479
+ * https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
480
+ """
481
+
482
+ _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
483
+ _refresh_env_var = "GIT_PYTHON_REFRESH"
484
+
485
+ GIT_PYTHON_GIT_EXECUTABLE = None
486
+ """Provide the full path to the git executable. Otherwise it assumes git is in the
487
+ executable search path.
488
+
489
+ :note:
490
+ The git executable is actually found during the refresh step in the top level
491
+ ``__init__``. It can also be changed by explicitly calling :func:`git.refresh`.
492
+ """
493
+
494
+ _refresh_token = object() # Since None would match an initial _version_info_token.
495
+
496
+ @classmethod
497
+ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
498
+ """Update information about the git executable :class:`Git` objects will use.
499
+
500
+ Called by the :func:`git.refresh` function in the top level ``__init__``.
501
+
502
+ :param path:
503
+ Optional path to the git executable. If not absolute, it is resolved
504
+ immediately, relative to the current directory. (See note below.)
505
+
506
+ :note:
507
+ The top-level :func:`git.refresh` should be preferred because it calls this
508
+ method and may also update other state accordingly.
509
+
510
+ :note:
511
+ There are three different ways to specify the command that refreshing causes
512
+ to be used for git:
513
+
514
+ 1. Pass no `path` argument and do not set the
515
+ :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable. The command
516
+ name ``git`` is used. It is looked up in a path search by the system, in
517
+ each command run (roughly similar to how git is found when running
518
+ ``git`` commands manually). This is usually the desired behavior.
519
+
520
+ 2. Pass no `path` argument but set the :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
521
+ environment variable. The command given as the value of that variable is
522
+ used. This may be a simple command or an arbitrary path. It is looked up
523
+ in each command run. Setting :envvar:`GIT_PYTHON_GIT_EXECUTABLE` to
524
+ ``git`` has the same effect as not setting it.
525
+
526
+ 3. Pass a `path` argument. This path, if not absolute, is immediately
527
+ resolved, relative to the current directory. This resolution occurs at
528
+ the time of the refresh. When git commands are run, they are run using
529
+ that previously resolved path. If a `path` argument is passed, the
530
+ :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable is not
531
+ consulted.
532
+
533
+ :note:
534
+ Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
535
+ attribute, which can be read on the :class:`Git` class or any of its
536
+ instances to check what command is used to run git. This attribute should
537
+ not be confused with the related :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
538
+ environment variable. The class attribute is set no matter how refreshing is
539
+ performed.
540
+ """
541
+ # Discern which path to refresh with.
542
+ if path is not None:
543
+ new_git = os.path.expanduser(path)
544
+ new_git = os.path.abspath(new_git)
545
+ else:
546
+ new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name)
547
+
548
+ # Keep track of the old and new git executable path.
549
+ old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
550
+ old_refresh_token = cls._refresh_token
551
+ cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
552
+ cls._refresh_token = object()
553
+
554
+ # Test if the new git executable path is valid. A GitCommandNotFound error is
555
+ # raised by us. A PermissionError is raised if the git executable cannot be
556
+ # executed for whatever reason.
557
+ has_git = False
558
+ try:
559
+ cls().version()
560
+ has_git = True
561
+ except (GitCommandNotFound, PermissionError):
562
+ pass
563
+
564
+ # Warn or raise exception if test failed.
565
+ if not has_git:
566
+ err = (
567
+ dedent(
568
+ """\
569
+ Bad git executable.
570
+ The git executable must be specified in one of the following ways:
571
+ - be included in your $PATH
572
+ - be set via $%s
573
+ - explicitly set via git.refresh(<full-path-to-git-executable>)
574
+ """
575
+ )
576
+ % cls._git_exec_env_var
577
+ )
578
+
579
+ # Revert to whatever the old_git was.
580
+ cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
581
+ cls._refresh_token = old_refresh_token
582
+
583
+ if old_git is None:
584
+ # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
585
+ # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
586
+
587
+ # Determine what the user wants to happen during the initial refresh. We
588
+ # expect GIT_PYTHON_REFRESH to either be unset or be one of the
589
+ # following values:
590
+ #
591
+ # 0|q|quiet|s|silence|silent|n|none
592
+ # 1|w|warn|warning|l|log
593
+ # 2|r|raise|e|error|exception
594
+
595
+ mode = os.environ.get(cls._refresh_env_var, "raise").lower()
596
+
597
+ quiet = ["quiet", "q", "silence", "s", "silent", "none", "n", "0"]
598
+ warn = ["warn", "w", "warning", "log", "l", "1"]
599
+ error = ["error", "e", "exception", "raise", "r", "2"]
600
+
601
+ if mode in quiet:
602
+ pass
603
+ elif mode in warn or mode in error:
604
+ err = dedent(
605
+ """\
606
+ %s
607
+ All git commands will error until this is rectified.
608
+
609
+ This initial message can be silenced or aggravated in the future by setting the
610
+ $%s environment variable. Use one of the following values:
611
+ - %s: for no message or exception
612
+ - %s: for a warning message (logging level CRITICAL, displayed by default)
613
+ - %s: for a raised exception
614
+
615
+ Example:
616
+ export %s=%s
617
+ """
618
+ ) % (
619
+ err,
620
+ cls._refresh_env_var,
621
+ "|".join(quiet),
622
+ "|".join(warn),
623
+ "|".join(error),
624
+ cls._refresh_env_var,
625
+ quiet[0],
626
+ )
627
+
628
+ if mode in warn:
629
+ _logger.critical(err)
630
+ else:
631
+ raise ImportError(err)
632
+ else:
633
+ err = dedent(
634
+ """\
635
+ %s environment variable has been set but it has been set with an invalid value.
636
+
637
+ Use only the following values:
638
+ - %s: for no message or exception
639
+ - %s: for a warning message (logging level CRITICAL, displayed by default)
640
+ - %s: for a raised exception
641
+ """
642
+ ) % (
643
+ cls._refresh_env_var,
644
+ "|".join(quiet),
645
+ "|".join(warn),
646
+ "|".join(error),
647
+ )
648
+ raise ImportError(err)
649
+
650
+ # We get here if this was the initial refresh and the refresh mode was
651
+ # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
652
+ # discern the difference between the first refresh at import time
653
+ # and subsequent calls to git.refresh or this refresh method.
654
+ cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
655
+ else:
656
+ # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
657
+ # None) we raise an exception.
658
+ raise GitCommandNotFound(new_git, err)
659
+
660
+ return has_git
661
+
662
+ @classmethod
663
+ def is_cygwin(cls) -> bool:
664
+ return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE)
665
+
666
+ @overload
667
+ @classmethod
668
+ def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ...
669
+
670
+ @overload
671
+ @classmethod
672
+ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ...
673
+
674
+ @classmethod
675
+ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
676
+ """Remove any backslashes from URLs to be written in config files.
677
+
678
+ Windows might create config files containing paths with backslashes, but git
679
+ stops liking them as it will escape the backslashes. Hence we undo the escaping
680
+ just to be sure.
681
+ """
682
+ if is_cygwin is None:
683
+ is_cygwin = cls.is_cygwin()
684
+
685
+ if is_cygwin:
686
+ url = cygpath(url)
687
+ else:
688
+ url = os.path.expandvars(url)
689
+ if url.startswith("~"):
690
+ url = os.path.expanduser(url)
691
+ url = url.replace("\\\\", "\\").replace("\\", "/")
692
+ return url
693
+
694
+ @classmethod
695
+ def check_unsafe_protocols(cls, url: str) -> None:
696
+ """Check for unsafe protocols.
697
+
698
+ Apart from the usual protocols (http, git, ssh), Git allows "remote helpers"
699
+ that have the form ``<transport>::<address>``. One of these helpers (``ext::``)
700
+ can be used to invoke any arbitrary command.
701
+
702
+ See:
703
+
704
+ - https://git-scm.com/docs/gitremote-helpers
705
+ - https://git-scm.com/docs/git-remote-ext
706
+ """
707
+ match = cls.re_unsafe_protocol.match(url)
708
+ if match:
709
+ protocol = match.group(1)
710
+ raise UnsafeProtocolError(
711
+ f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
712
+ )
713
+
714
+ @classmethod
715
+ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
716
+ """Check for unsafe options.
717
+
718
+ Some options that are passed to ``git <command>`` can be used to execute
719
+ arbitrary commands. These are blocked by default.
720
+ """
721
+ # Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
722
+ # check if they start with "--foo" or if they are equal to "foo".
723
+ bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
724
+ for option in options:
725
+ for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
726
+ if option.startswith(unsafe_option) or option == bare_option:
727
+ raise UnsafeOptionError(
728
+ f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
729
+ )
730
+
731
+ class AutoInterrupt:
732
+ """Process wrapper that terminates the wrapped process on finalization.
733
+
734
+ This kills/interrupts the stored process instance once this instance goes out of
735
+ scope. It is used to prevent processes piling up in case iterators stop reading.
736
+
737
+ All attributes are wired through to the contained process object.
738
+
739
+ The wait method is overridden to perform automatic status code checking and
740
+ possibly raise.
741
+ """
742
+
743
+ __slots__ = ("proc", "args", "status")
744
+
745
+ # If this is non-zero it will override any status code during _terminate, used
746
+ # to prevent race conditions in testing.
747
+ _status_code_if_terminate: int = 0
748
+
749
+ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
750
+ self.proc = proc
751
+ self.args = args
752
+ self.status: Union[int, None] = None
753
+
754
+ def _terminate(self) -> None:
755
+ """Terminate the underlying process."""
756
+ if self.proc is None:
757
+ return
758
+
759
+ proc = self.proc
760
+ self.proc = None
761
+ if proc.stdin:
762
+ proc.stdin.close()
763
+ if proc.stdout:
764
+ proc.stdout.close()
765
+ if proc.stderr:
766
+ proc.stderr.close()
767
+ # Did the process finish already so we have a return code?
768
+ try:
769
+ if proc.poll() is not None:
770
+ self.status = self._status_code_if_terminate or proc.poll()
771
+ return
772
+ except OSError as ex:
773
+ _logger.info("Ignored error after process had died: %r", ex)
774
+
775
+ # It can be that nothing really exists anymore...
776
+ if os is None or getattr(os, "kill", None) is None:
777
+ return
778
+
779
+ # Try to kill it.
780
+ try:
781
+ proc.terminate()
782
+ status = proc.wait() # Ensure the process goes away.
783
+
784
+ self.status = self._status_code_if_terminate or status
785
+ except OSError as ex:
786
+ _logger.info("Ignored error after process had died: %r", ex)
787
+ # END exception handling
788
+
789
+ def __del__(self) -> None:
790
+ self._terminate()
791
+
792
+ def __getattr__(self, attr: str) -> Any:
793
+ return getattr(self.proc, attr)
794
+
795
+ # TODO: Bad choice to mimic `proc.wait()` but with different args.
796
+ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
797
+ """Wait for the process and return its status code.
798
+
799
+ :param stderr:
800
+ Previously read value of stderr, in case stderr is already closed.
801
+
802
+ :warn:
803
+ May deadlock if output or error pipes are used and not handled
804
+ separately.
805
+
806
+ :raise git.exc.GitCommandError:
807
+ If the return status is not 0.
808
+ """
809
+ if stderr is None:
810
+ stderr_b = b""
811
+ stderr_b = force_bytes(data=stderr, encoding="utf-8")
812
+ status: Union[int, None]
813
+ if self.proc is not None:
814
+ status = self.proc.wait()
815
+ p_stderr = self.proc.stderr
816
+ else: # Assume the underlying proc was killed earlier or never existed.
817
+ status = self.status
818
+ p_stderr = None
819
+
820
+ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
821
+ if stream:
822
+ try:
823
+ return stderr_b + force_bytes(stream.read())
824
+ except (OSError, ValueError):
825
+ return stderr_b or b""
826
+ else:
827
+ return stderr_b or b""
828
+
829
+ # END status handling
830
+
831
+ if status != 0:
832
+ errstr = read_all_from_possibly_closed_stream(p_stderr)
833
+ _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
834
+ raise GitCommandError(remove_password_if_present(self.args), status, errstr)
835
+ return status
836
+
837
+ # END auto interrupt
838
+
839
+ class CatFileContentStream:
840
+ """Object representing a sized read-only stream returning the contents of
841
+ an object.
842
+
843
+ This behaves like a stream, but counts the data read and simulates an empty
844
+ stream once our sized content region is empty.
845
+
846
+ If not all data are read to the end of the object's lifetime, we read the
847
+ rest to ensure the underlying stream continues to work.
848
+ """
849
+
850
+ __slots__ = ("_stream", "_nbr", "_size")
851
+
852
+ def __init__(self, size: int, stream: IO[bytes]) -> None:
853
+ self._stream = stream
854
+ self._size = size
855
+ self._nbr = 0 # Number of bytes read.
856
+
857
+ # Special case: If the object is empty, has null bytes, get the final
858
+ # newline right away.
859
+ if size == 0:
860
+ stream.read(1)
861
+ # END handle empty streams
862
+
863
+ def read(self, size: int = -1) -> bytes:
864
+ bytes_left = self._size - self._nbr
865
+ if bytes_left == 0:
866
+ return b""
867
+ if size > -1:
868
+ # Ensure we don't try to read past our limit.
869
+ size = min(bytes_left, size)
870
+ else:
871
+ # They try to read all, make sure it's not more than what remains.
872
+ size = bytes_left
873
+ # END check early depletion
874
+ data = self._stream.read(size)
875
+ self._nbr += len(data)
876
+
877
+ # Check for depletion, read our final byte to make the stream usable by
878
+ # others.
879
+ if self._size - self._nbr == 0:
880
+ self._stream.read(1) # final newline
881
+ # END finish reading
882
+ return data
883
+
884
+ def readline(self, size: int = -1) -> bytes:
885
+ if self._nbr == self._size:
886
+ return b""
887
+
888
+ # Clamp size to lowest allowed value.
889
+ bytes_left = self._size - self._nbr
890
+ if size > -1:
891
+ size = min(bytes_left, size)
892
+ else:
893
+ size = bytes_left
894
+ # END handle size
895
+
896
+ data = self._stream.readline(size)
897
+ self._nbr += len(data)
898
+
899
+ # Handle final byte.
900
+ if self._size - self._nbr == 0:
901
+ self._stream.read(1)
902
+ # END finish reading
903
+
904
+ return data
905
+
906
+ def readlines(self, size: int = -1) -> List[bytes]:
907
+ if self._nbr == self._size:
908
+ return []
909
+
910
+ # Leave all additional logic to our readline method, we just check the size.
911
+ out = []
912
+ nbr = 0
913
+ while True:
914
+ line = self.readline()
915
+ if not line:
916
+ break
917
+ out.append(line)
918
+ if size > -1:
919
+ nbr += len(line)
920
+ if nbr > size:
921
+ break
922
+ # END handle size constraint
923
+ # END readline loop
924
+ return out
925
+
926
+ # skipcq: PYL-E0301
927
+ def __iter__(self) -> "Git.CatFileContentStream":
928
+ return self
929
+
930
+ def __next__(self) -> bytes:
931
+ line = self.readline()
932
+ if not line:
933
+ raise StopIteration
934
+
935
+ return line
936
+
937
+ next = __next__
938
+
939
+ def __del__(self) -> None:
940
+ bytes_left = self._size - self._nbr
941
+ if bytes_left:
942
+ # Read and discard - seeking is impossible within a stream.
943
+ # This includes any terminating newline.
944
+ self._stream.read(bytes_left + 1)
945
+ # END handle incomplete read
946
+
947
+ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
948
+ """Initialize this instance with:
949
+
950
+ :param working_dir:
951
+ Git directory we should work in. If ``None``, we always work in the current
952
+ directory as returned by :func:`os.getcwd`.
953
+ This is meant to be the working tree directory if available, or the
954
+ ``.git`` directory in case of bare repositories.
955
+ """
956
+ super().__init__()
957
+ self._working_dir = expand_path(working_dir)
958
+ self._git_options: Union[List[str], Tuple[str, ...]] = ()
959
+ self._persistent_git_options: List[str] = []
960
+
961
+ # Extra environment variables to pass to git commands
962
+ self._environment: Dict[str, str] = {}
963
+
964
+ # Cached version slots
965
+ self._version_info: Union[Tuple[int, ...], None] = None
966
+ self._version_info_token: object = None
967
+
968
+ # Cached command slots
969
+ self.cat_file_header: Union[None, TBD] = None
970
+ self.cat_file_all: Union[None, TBD] = None
971
+
972
+ def __getattribute__(self, name: str) -> Any:
973
+ if name == "USE_SHELL":
974
+ _warn_use_shell(False)
975
+ return super().__getattribute__(name)
976
+
977
+ def __getattr__(self, name: str) -> Any:
978
+ """A convenience method as it allows to call the command as if it was an object.
979
+
980
+ :return:
981
+ Callable object that will execute call :meth:`_call_process` with your
982
+ arguments.
983
+ """
984
+ if name.startswith("_"):
985
+ return super().__getattribute__(name)
986
+ return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
987
+
988
+ def set_persistent_git_options(self, **kwargs: Any) -> None:
989
+ """Specify command line options to the git executable for subsequent
990
+ subcommand calls.
991
+
992
+ :param kwargs:
993
+ A dict of keyword arguments.
994
+ These arguments are passed as in :meth:`_call_process`, but will be passed
995
+ to the git command rather than the subcommand.
996
+ """
997
+
998
+ self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
999
+
1000
+ @property
1001
+ def working_dir(self) -> Union[None, PathLike]:
1002
+ """:return: Git directory we are working on"""
1003
+ return self._working_dir
1004
+
1005
+ @property
1006
+ def version_info(self) -> Tuple[int, ...]:
1007
+ """
1008
+ :return: Tuple with integers representing the major, minor and additional
1009
+ version numbers as parsed from :manpage:`git-version(1)`. Up to four fields
1010
+ are used.
1011
+
1012
+ This value is generated on demand and is cached.
1013
+ """
1014
+ # Refreshing is global, but version_info caching is per-instance.
1015
+ refresh_token = self._refresh_token # Copy token in case of concurrent refresh.
1016
+
1017
+ # Use the cached version if obtained after the most recent refresh.
1018
+ if self._version_info_token is refresh_token:
1019
+ assert self._version_info is not None, "Bug: corrupted token-check state"
1020
+ return self._version_info
1021
+
1022
+ # Run "git version" and parse it.
1023
+ process_version = self._call_process("version")
1024
+ version_string = process_version.split(" ")[2]
1025
+ version_fields = version_string.split(".")[:4]
1026
+ leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields)
1027
+ self._version_info = tuple(map(int, leading_numeric_fields))
1028
+
1029
+ # This value will be considered valid until the next refresh.
1030
+ self._version_info_token = refresh_token
1031
+ return self._version_info
1032
+
1033
+ @overload
1034
+ def execute(
1035
+ self,
1036
+ command: Union[str, Sequence[Any]],
1037
+ *,
1038
+ as_process: Literal[True],
1039
+ ) -> "AutoInterrupt": ...
1040
+
1041
+ @overload
1042
+ def execute(
1043
+ self,
1044
+ command: Union[str, Sequence[Any]],
1045
+ *,
1046
+ as_process: Literal[False] = False,
1047
+ stdout_as_string: Literal[True],
1048
+ ) -> Union[str, Tuple[int, str, str]]: ...
1049
+
1050
+ @overload
1051
+ def execute(
1052
+ self,
1053
+ command: Union[str, Sequence[Any]],
1054
+ *,
1055
+ as_process: Literal[False] = False,
1056
+ stdout_as_string: Literal[False] = False,
1057
+ ) -> Union[bytes, Tuple[int, bytes, str]]: ...
1058
+
1059
+ @overload
1060
+ def execute(
1061
+ self,
1062
+ command: Union[str, Sequence[Any]],
1063
+ *,
1064
+ with_extended_output: Literal[False],
1065
+ as_process: Literal[False],
1066
+ stdout_as_string: Literal[True],
1067
+ ) -> str: ...
1068
+
1069
+ @overload
1070
+ def execute(
1071
+ self,
1072
+ command: Union[str, Sequence[Any]],
1073
+ *,
1074
+ with_extended_output: Literal[False],
1075
+ as_process: Literal[False],
1076
+ stdout_as_string: Literal[False],
1077
+ ) -> bytes: ...
1078
+
1079
+ def execute(
1080
+ self,
1081
+ command: Union[str, Sequence[Any]],
1082
+ istream: Union[None, BinaryIO] = None,
1083
+ with_extended_output: bool = False,
1084
+ with_exceptions: bool = True,
1085
+ as_process: bool = False,
1086
+ output_stream: Union[None, BinaryIO] = None,
1087
+ stdout_as_string: bool = True,
1088
+ kill_after_timeout: Union[None, float] = None,
1089
+ with_stdout: bool = True,
1090
+ universal_newlines: bool = False,
1091
+ shell: Union[None, bool] = None,
1092
+ env: Union[None, Mapping[str, str]] = None,
1093
+ max_chunk_size: int = io.DEFAULT_BUFFER_SIZE,
1094
+ strip_newline_in_stdout: bool = True,
1095
+ **subprocess_kwargs: Any,
1096
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
1097
+ R"""Handle executing the command, and consume and return the returned
1098
+ information (stdout).
1099
+
1100
+ :param command:
1101
+ The command argument list to execute.
1102
+ It should be a sequence of program arguments, or a string. The
1103
+ program to execute is the first item in the args sequence or string.
1104
+
1105
+ :param istream:
1106
+ Standard input filehandle passed to :class:`subprocess.Popen`.
1107
+
1108
+ :param with_extended_output:
1109
+ Whether to return a (status, stdout, stderr) tuple.
1110
+
1111
+ :param with_exceptions:
1112
+ Whether to raise an exception when git returns a non-zero status.
1113
+
1114
+ :param as_process:
1115
+ Whether to return the created process instance directly from which
1116
+ streams can be read on demand. This will render `with_extended_output`
1117
+ and `with_exceptions` ineffective - the caller will have to deal with
1118
+ the details. It is important to note that the process will be placed
1119
+ into an :class:`AutoInterrupt` wrapper that will interrupt the process
1120
+ once it goes out of scope. If you use the command in iterators, you
1121
+ should pass the whole process instance instead of a single stream.
1122
+
1123
+ :param output_stream:
1124
+ If set to a file-like object, data produced by the git command will be
1125
+ copied to the given stream instead of being returned as a string.
1126
+ This feature only has any effect if `as_process` is ``False``.
1127
+
1128
+ :param stdout_as_string:
1129
+ If ``False``, the command's standard output will be bytes. Otherwise, it
1130
+ will be decoded into a string using the default encoding (usually UTF-8).
1131
+ The latter can fail, if the output contains binary data.
1132
+
1133
+ :param kill_after_timeout:
1134
+ Specifies a timeout in seconds for the git command, after which the process
1135
+ should be killed. This will have no effect if `as_process` is set to
1136
+ ``True``. It is set to ``None`` by default and will let the process run
1137
+ until the timeout is explicitly specified. Uses of this feature should be
1138
+ carefully considered, due to the following limitations:
1139
+
1140
+ 1. This feature is not supported at all on Windows.
1141
+ 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
1142
+ enumerate child processes, which is available on most GNU/Linux systems
1143
+ but not most others.
1144
+ 3. Deeper descendants do not receive signals, though they may sometimes
1145
+ terminate as a consequence of their parent processes being killed.
1146
+ 4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side
1147
+ effects on a repository. For example, stale locks in case of
1148
+ :manpage:`git-gc(1)` could render the repository incapable of accepting
1149
+ changes until the lock is manually removed.
1150
+
1151
+ :param with_stdout:
1152
+ If ``True``, default ``True``, we open stdout on the created process.
1153
+
1154
+ :param universal_newlines:
1155
+ If ``True``, pipes will be opened as text, and lines are split at all known
1156
+ line endings.
1157
+
1158
+ :param shell:
1159
+ Whether to invoke commands through a shell
1160
+ (see :class:`Popen(..., shell=True) <subprocess.Popen>`).
1161
+ If this is not ``None``, it overrides :attr:`USE_SHELL`.
1162
+
1163
+ Passing ``shell=True`` to this or any other GitPython function should be
1164
+ avoided, as it is unsafe under most circumstances. This is because it is
1165
+ typically not feasible to fully consider and account for the effect of shell
1166
+ expansions, especially when passing ``shell=True`` to other methods that
1167
+ forward it to :meth:`Git.execute`. Passing ``shell=True`` is also no longer
1168
+ needed (nor useful) to work around any known operating system specific
1169
+ issues.
1170
+
1171
+ :param env:
1172
+ A dictionary of environment variables to be passed to
1173
+ :class:`subprocess.Popen`.
1174
+
1175
+ :param max_chunk_size:
1176
+ Maximum number of bytes in one chunk of data passed to the `output_stream`
1177
+ in one invocation of its ``write()`` method. If the given number is not
1178
+ positive then the default value is used.
1179
+
1180
+ :param strip_newline_in_stdout:
1181
+ Whether to strip the trailing ``\n`` of the command stdout.
1182
+
1183
+ :param subprocess_kwargs:
1184
+ Keyword arguments to be passed to :class:`subprocess.Popen`. Please note
1185
+ that some of the valid kwargs are already set by this method; the ones you
1186
+ specify may not be the same ones.
1187
+
1188
+ :return:
1189
+ * str(output), if `extended_output` is ``False`` (Default)
1190
+ * tuple(int(status), str(stdout), str(stderr)),
1191
+ if `extended_output` is ``True``
1192
+
1193
+ If `output_stream` is ``True``, the stdout value will be your output stream:
1194
+
1195
+ * output_stream, if `extended_output` is ``False``
1196
+ * tuple(int(status), output_stream, str(stderr)),
1197
+ if `extended_output` is ``True``
1198
+
1199
+ Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
1200
+ output regardless of system language.
1201
+
1202
+ :raise git.exc.GitCommandError:
1203
+
1204
+ :note:
1205
+ If you add additional keyword arguments to the signature of this method, you
1206
+ must update the ``execute_kwargs`` variable housed in this module.
1207
+ """
1208
+ # Remove password for the command if present.
1209
+ redacted_command = remove_password_if_present(command)
1210
+ if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
1211
+ _logger.info(" ".join(redacted_command))
1212
+
1213
+ # Allow the user to have the command executed in their working dir.
1214
+ try:
1215
+ cwd = self._working_dir or os.getcwd() # type: Union[None, str]
1216
+ if not os.access(str(cwd), os.X_OK):
1217
+ cwd = None
1218
+ except FileNotFoundError:
1219
+ cwd = None
1220
+
1221
+ # Start the process.
1222
+ inline_env = env
1223
+ env = os.environ.copy()
1224
+ # Attempt to force all output to plain ASCII English, which is what some parsing
1225
+ # code may expect.
1226
+ # According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well
1227
+ # just to be sure.
1228
+ env["LANGUAGE"] = "C"
1229
+ env["LC_ALL"] = "C"
1230
+ env.update(self._environment)
1231
+ if inline_env is not None:
1232
+ env.update(inline_env)
1233
+
1234
+ if sys.platform == "win32":
1235
+ if kill_after_timeout is not None:
1236
+ raise GitCommandError(
1237
+ redacted_command,
1238
+ '"kill_after_timeout" feature is not supported on Windows.',
1239
+ )
1240
+ cmd_not_found_exception = OSError
1241
+ else:
1242
+ cmd_not_found_exception = FileNotFoundError
1243
+ # END handle
1244
+
1245
+ stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
1246
+ if shell is None:
1247
+ # Get the value of USE_SHELL with no deprecation warning. Do this without
1248
+ # warnings.catch_warnings, to avoid a race condition with application code
1249
+ # configuring warnings. The value could be looked up in type(self).__dict__
1250
+ # or Git.__dict__, but those can break under some circumstances. This works
1251
+ # the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1252
+ shell = super().__getattribute__("USE_SHELL")
1253
+ _logger.debug(
1254
+ "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
1255
+ redacted_command,
1256
+ cwd,
1257
+ "<valid stream>" if istream else "None",
1258
+ shell,
1259
+ universal_newlines,
1260
+ )
1261
+ try:
1262
+ proc = safer_popen(
1263
+ command,
1264
+ env=env,
1265
+ cwd=cwd,
1266
+ bufsize=-1,
1267
+ stdin=(istream or DEVNULL),
1268
+ stderr=PIPE,
1269
+ stdout=stdout_sink,
1270
+ shell=shell,
1271
+ universal_newlines=universal_newlines,
1272
+ encoding=defenc if universal_newlines else None,
1273
+ **subprocess_kwargs,
1274
+ )
1275
+ except cmd_not_found_exception as err:
1276
+ raise GitCommandNotFound(redacted_command, err) from err
1277
+ else:
1278
+ # Replace with a typeguard for Popen[bytes]?
1279
+ proc.stdout = cast(BinaryIO, proc.stdout)
1280
+ proc.stderr = cast(BinaryIO, proc.stderr)
1281
+
1282
+ if as_process:
1283
+ return self.AutoInterrupt(proc, command)
1284
+
1285
+ if sys.platform != "win32" and kill_after_timeout is not None:
1286
+ # Help mypy figure out this is not None even when used inside communicate().
1287
+ timeout = kill_after_timeout
1288
+
1289
+ def kill_process(pid: int) -> None:
1290
+ """Callback to kill a process.
1291
+
1292
+ This callback implementation would be ineffective and unsafe on Windows.
1293
+ """
1294
+ p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
1295
+ child_pids = []
1296
+ if p.stdout is not None:
1297
+ for line in p.stdout:
1298
+ if len(line.split()) > 0:
1299
+ local_pid = (line.split())[0]
1300
+ if local_pid.isdigit():
1301
+ child_pids.append(int(local_pid))
1302
+ try:
1303
+ os.kill(pid, signal.SIGKILL)
1304
+ for child_pid in child_pids:
1305
+ try:
1306
+ os.kill(child_pid, signal.SIGKILL)
1307
+ except OSError:
1308
+ pass
1309
+ # Tell the main routine that the process was killed.
1310
+ kill_check.set()
1311
+ except OSError:
1312
+ # It is possible that the process gets completed in the duration
1313
+ # after timeout happens and before we try to kill the process.
1314
+ pass
1315
+ return
1316
+
1317
+ def communicate() -> Tuple[AnyStr, AnyStr]:
1318
+ watchdog.start()
1319
+ out, err = proc.communicate()
1320
+ watchdog.cancel()
1321
+ if kill_check.is_set():
1322
+ err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
1323
+ " ".join(redacted_command),
1324
+ timeout,
1325
+ )
1326
+ if not universal_newlines:
1327
+ err = err.encode(defenc)
1328
+ return out, err
1329
+
1330
+ # END helpers
1331
+
1332
+ kill_check = threading.Event()
1333
+ watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
1334
+ else:
1335
+ communicate = proc.communicate
1336
+
1337
+ # Wait for the process to return.
1338
+ status = 0
1339
+ stdout_value: Union[str, bytes] = b""
1340
+ stderr_value: Union[str, bytes] = b""
1341
+ newline = "\n" if universal_newlines else b"\n"
1342
+ try:
1343
+ if output_stream is None:
1344
+ stdout_value, stderr_value = communicate()
1345
+ # Strip trailing "\n".
1346
+ if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
1347
+ stdout_value = stdout_value[:-1]
1348
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1349
+ stderr_value = stderr_value[:-1]
1350
+
1351
+ status = proc.returncode
1352
+ else:
1353
+ max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
1354
+ stream_copy(proc.stdout, output_stream, max_chunk_size)
1355
+ stdout_value = proc.stdout.read()
1356
+ stderr_value = proc.stderr.read()
1357
+ # Strip trailing "\n".
1358
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1359
+ stderr_value = stderr_value[:-1]
1360
+ status = proc.wait()
1361
+ # END stdout handling
1362
+ finally:
1363
+ proc.stdout.close()
1364
+ proc.stderr.close()
1365
+
1366
+ if self.GIT_PYTHON_TRACE == "full":
1367
+ cmdstr = " ".join(redacted_command)
1368
+
1369
+ def as_text(stdout_value: Union[bytes, str]) -> str:
1370
+ return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
1371
+
1372
+ # END as_text
1373
+
1374
+ if stderr_value:
1375
+ _logger.info(
1376
+ "%s -> %d; stdout: '%s'; stderr: '%s'",
1377
+ cmdstr,
1378
+ status,
1379
+ as_text(stdout_value),
1380
+ safe_decode(stderr_value),
1381
+ )
1382
+ elif stdout_value:
1383
+ _logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
1384
+ else:
1385
+ _logger.info("%s -> %d", cmdstr, status)
1386
+ # END handle debug printing
1387
+
1388
+ if with_exceptions and status != 0:
1389
+ raise GitCommandError(redacted_command, status, stderr_value, stdout_value)
1390
+
1391
+ if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream.
1392
+ stdout_value = safe_decode(stdout_value)
1393
+
1394
+ # Allow access to the command's status code.
1395
+ if with_extended_output:
1396
+ return (status, stdout_value, safe_decode(stderr_value))
1397
+ else:
1398
+ return stdout_value
1399
+
1400
+ def environment(self) -> Dict[str, str]:
1401
+ return self._environment
1402
+
1403
+ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
1404
+ """Set environment variables for future git invocations. Return all changed
1405
+ values in a format that can be passed back into this function to revert the
1406
+ changes.
1407
+
1408
+ Examples::
1409
+
1410
+ old_env = self.update_environment(PWD='/tmp')
1411
+ self.update_environment(**old_env)
1412
+
1413
+ :param kwargs:
1414
+ Environment variables to use for git processes.
1415
+
1416
+ :return:
1417
+ Dict that maps environment variables to their old values
1418
+ """
1419
+ old_env = {}
1420
+ for key, value in kwargs.items():
1421
+ # Set value if it is None.
1422
+ if value is not None:
1423
+ old_env[key] = self._environment.get(key)
1424
+ self._environment[key] = value
1425
+ # Remove key from environment if its value is None.
1426
+ elif key in self._environment:
1427
+ old_env[key] = self._environment[key]
1428
+ del self._environment[key]
1429
+ return old_env
1430
+
1431
+ @contextlib.contextmanager
1432
+ def custom_environment(self, **kwargs: Any) -> Iterator[None]:
1433
+ """A context manager around the above :meth:`update_environment` method to
1434
+ restore the environment back to its previous state after operation.
1435
+
1436
+ Examples::
1437
+
1438
+ with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
1439
+ repo.remotes.origin.fetch()
1440
+
1441
+ :param kwargs:
1442
+ See :meth:`update_environment`.
1443
+ """
1444
+ old_env = self.update_environment(**kwargs)
1445
+ try:
1446
+ yield
1447
+ finally:
1448
+ self.update_environment(**old_env)
1449
+
1450
+ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]:
1451
+ if len(name) == 1:
1452
+ if value is True:
1453
+ return ["-%s" % name]
1454
+ elif value not in (False, None):
1455
+ if split_single_char_options:
1456
+ return ["-%s" % name, "%s" % value]
1457
+ else:
1458
+ return ["-%s%s" % (name, value)]
1459
+ else:
1460
+ if value is True:
1461
+ return ["--%s" % dashify(name)]
1462
+ elif value is not False and value is not None:
1463
+ return ["--%s=%s" % (dashify(name), value)]
1464
+ return []
1465
+
1466
+ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
1467
+ """Transform Python-style kwargs into git command line options."""
1468
+ args = []
1469
+ for k, v in kwargs.items():
1470
+ if isinstance(v, (list, tuple)):
1471
+ for value in v:
1472
+ args += self.transform_kwarg(k, value, split_single_char_options)
1473
+ else:
1474
+ args += self.transform_kwarg(k, v, split_single_char_options)
1475
+ return args
1476
+
1477
+ @classmethod
1478
+ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
1479
+ outlist = []
1480
+ if isinstance(arg_list, (list, tuple)):
1481
+ for arg in arg_list:
1482
+ outlist.extend(cls._unpack_args(arg))
1483
+ else:
1484
+ outlist.append(str(arg_list))
1485
+
1486
+ return outlist
1487
+
1488
+ def __call__(self, **kwargs: Any) -> "Git":
1489
+ """Specify command line options to the git executable for a subcommand call.
1490
+
1491
+ :param kwargs:
1492
+ A dict of keyword arguments.
1493
+ These arguments are passed as in :meth:`_call_process`, but will be passed
1494
+ to the git command rather than the subcommand.
1495
+
1496
+ Examples::
1497
+
1498
+ git(work_tree='/tmp').difftool()
1499
+ """
1500
+ self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
1501
+ return self
1502
+
1503
+ @overload
1504
+ def _call_process(
1505
+ self, method: str, *args: None, **kwargs: None
1506
+ ) -> str: ... # If no args were given, execute the call with all defaults.
1507
+
1508
+ @overload
1509
+ def _call_process(
1510
+ self,
1511
+ method: str,
1512
+ istream: int,
1513
+ as_process: Literal[True],
1514
+ *args: Any,
1515
+ **kwargs: Any,
1516
+ ) -> "Git.AutoInterrupt": ...
1517
+
1518
+ @overload
1519
+ def _call_process(
1520
+ self, method: str, *args: Any, **kwargs: Any
1521
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ...
1522
+
1523
+ def _call_process(
1524
+ self, method: str, *args: Any, **kwargs: Any
1525
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
1526
+ """Run the given git command with the specified arguments and return the result
1527
+ as a string.
1528
+
1529
+ :param method:
1530
+ The command. Contained ``_`` characters will be converted to hyphens, such
1531
+ as in ``ls_files`` to call ``ls-files``.
1532
+
1533
+ :param args:
1534
+ The list of arguments. If ``None`` is included, it will be pruned.
1535
+ This allows your commands to call git more conveniently, as ``None`` is
1536
+ realized as non-existent.
1537
+
1538
+ :param kwargs:
1539
+ Contains key-values for the following:
1540
+
1541
+ - The :meth:`execute()` kwds, as listed in ``execute_kwargs``.
1542
+ - "Command options" to be converted by :meth:`transform_kwargs`.
1543
+ - The ``insert_kwargs_after`` key which its value must match one of
1544
+ ``*args``.
1545
+
1546
+ It also contains any command options, to be appended after the matched arg.
1547
+
1548
+ Examples::
1549
+
1550
+ git.rev_list('master', max_count=10, header=True)
1551
+
1552
+ turns into::
1553
+
1554
+ git rev-list max-count 10 --header master
1555
+
1556
+ :return:
1557
+ Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
1558
+ default (especially ``as_process = False``, ``stdout_as_string = True``) and
1559
+ return :class:`str`.
1560
+ """
1561
+ # Handle optional arguments prior to calling transform_kwargs.
1562
+ # Otherwise these'll end up in args, which is bad.
1563
+ exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
1564
+ opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
1565
+
1566
+ insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None)
1567
+
1568
+ # Prepare the argument list.
1569
+
1570
+ opt_args = self.transform_kwargs(**opts_kwargs)
1571
+ ext_args = self._unpack_args([a for a in args if a is not None])
1572
+
1573
+ if insert_after_this_arg is None:
1574
+ args_list = opt_args + ext_args
1575
+ else:
1576
+ try:
1577
+ index = ext_args.index(insert_after_this_arg)
1578
+ except ValueError as err:
1579
+ raise ValueError(
1580
+ "Couldn't find argument '%s' in args %s to insert cmd options after"
1581
+ % (insert_after_this_arg, str(ext_args))
1582
+ ) from err
1583
+ # END handle error
1584
+ args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :]
1585
+ # END handle opts_kwargs
1586
+
1587
+ call = [self.GIT_PYTHON_GIT_EXECUTABLE]
1588
+
1589
+ # Add persistent git options.
1590
+ call.extend(self._persistent_git_options)
1591
+
1592
+ # Add the git options, then reset to empty to avoid side effects.
1593
+ call.extend(self._git_options)
1594
+ self._git_options = ()
1595
+
1596
+ call.append(dashify(method))
1597
+ call.extend(args_list)
1598
+
1599
+ return self.execute(call, **exec_kwargs)
1600
+
1601
+ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
1602
+ """
1603
+ :param header_line:
1604
+ A line of the form::
1605
+
1606
+ <hex_sha> type_string size_as_int
1607
+
1608
+ :return:
1609
+ (hex_sha, type_string, size_as_int)
1610
+
1611
+ :raise ValueError:
1612
+ If the header contains indication for an error due to incorrect input sha.
1613
+ """
1614
+ tokens = header_line.split()
1615
+ if len(tokens) != 3:
1616
+ if not tokens:
1617
+ err_msg = (
1618
+ f"SHA is empty, possible dubious ownership in the repository "
1619
+ f"""at {self._working_dir}.\n If this is unintended run:\n\n """
1620
+ f""" "git config --global --add safe.directory {self._working_dir}" """
1621
+ )
1622
+ raise ValueError(err_msg)
1623
+ else:
1624
+ raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip()))
1625
+ # END handle actual return value
1626
+ # END error handling
1627
+
1628
+ if len(tokens[0]) != 40:
1629
+ raise ValueError("Failed to parse header: %r" % header_line)
1630
+ return (tokens[0], tokens[1], int(tokens[2]))
1631
+
1632
+ def _prepare_ref(self, ref: AnyStr) -> bytes:
1633
+ # Required for command to separate refs on stdin, as bytes.
1634
+ if isinstance(ref, bytes):
1635
+ # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text.
1636
+ refstr: str = ref.decode("ascii")
1637
+ elif not isinstance(ref, str):
1638
+ refstr = str(ref) # Could be ref-object.
1639
+ else:
1640
+ refstr = ref
1641
+
1642
+ if not refstr.endswith("\n"):
1643
+ refstr += "\n"
1644
+ return refstr.encode(defenc)
1645
+
1646
+ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt":
1647
+ cur_val = getattr(self, attr_name)
1648
+ if cur_val is not None:
1649
+ return cur_val
1650
+
1651
+ options = {"istream": PIPE, "as_process": True}
1652
+ options.update(kwargs)
1653
+
1654
+ cmd = self._call_process(cmd_name, *args, **options)
1655
+ setattr(self, attr_name, cmd)
1656
+ cmd = cast("Git.AutoInterrupt", cmd)
1657
+ return cmd
1658
+
1659
+ def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]:
1660
+ if cmd.stdin and cmd.stdout:
1661
+ cmd.stdin.write(self._prepare_ref(ref))
1662
+ cmd.stdin.flush()
1663
+ return self._parse_object_header(cmd.stdout.readline())
1664
+ else:
1665
+ raise ValueError("cmd stdin was empty")
1666
+
1667
+ def get_object_header(self, ref: str) -> Tuple[str, str, int]:
1668
+ """Use this method to quickly examine the type and size of the object behind the
1669
+ given ref.
1670
+
1671
+ :note:
1672
+ The method will only suffer from the costs of command invocation once and
1673
+ reuses the command in subsequent calls.
1674
+
1675
+ :return:
1676
+ (hexsha, type_string, size_as_int)
1677
+ """
1678
+ cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
1679
+ return self.__get_object_header(cmd, ref)
1680
+
1681
+ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
1682
+ """Similar to :meth:`get_object_header`, but returns object data as well.
1683
+
1684
+ :return:
1685
+ (hexsha, type_string, size_as_int, data_string)
1686
+
1687
+ :note:
1688
+ Not threadsafe.
1689
+ """
1690
+ hexsha, typename, size, stream = self.stream_object_data(ref)
1691
+ data = stream.read(size)
1692
+ del stream
1693
+ return (hexsha, typename, size, data)
1694
+
1695
+ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
1696
+ """Similar to :meth:`get_object_data`, but returns the data as a stream.
1697
+
1698
+ :return:
1699
+ (hexsha, type_string, size_as_int, stream)
1700
+
1701
+ :note:
1702
+ This method is not threadsafe. You need one independent :class:`Git`
1703
+ instance per thread to be safe!
1704
+ """
1705
+ cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
1706
+ hexsha, typename, size = self.__get_object_header(cmd, ref)
1707
+ cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()
1708
+ return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout))
1709
+
1710
+ def clear_cache(self) -> "Git":
1711
+ """Clear all kinds of internal caches to release resources.
1712
+
1713
+ Currently persistent commands will be interrupted.
1714
+
1715
+ :return:
1716
+ self
1717
+ """
1718
+ for cmd in (self.cat_file_all, self.cat_file_header):
1719
+ if cmd:
1720
+ cmd.__del__()
1721
+
1722
+ self.cat_file_all = None
1723
+ self.cat_file_header = None
1724
+ return self
venv/lib/python3.10/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.10/site-packages/git/config.py ADDED
@@ -0,0 +1,944 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 name, method in methods:
91
+ if name in clsdict:
92
+ continue
93
+ method_with_values = needs_values(method)
94
+ if name in mutating_methods:
95
+ method_with_values = set_dirty_and_flush_changes(method_with_values)
96
+ # END mutating methods handling
97
+
98
+ clsdict[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
+ if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
500
+ pos = optval.find(";")
501
+ if pos != -1 and optval[pos - 1].isspace():
502
+ optval = optval[:pos]
503
+ optval = optval.strip()
504
+ if optval == '""':
505
+ optval = ""
506
+ # END handle empty string
507
+ optname = self.optionxform(optname.rstrip())
508
+ if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
509
+ is_multi_line = True
510
+ optval = string_decode(optval[1:])
511
+ # END handle multi-line
512
+ # Preserves multiple values for duplicate optnames.
513
+ cursect.add(optname, optval)
514
+ else:
515
+ # Check if it's an option with no value - it's just ignored by git.
516
+ if not self.OPTVALUEONLY.match(line):
517
+ if not e:
518
+ e = cp.ParsingError(fpname)
519
+ e.append(lineno, repr(line))
520
+ continue
521
+ else:
522
+ line = line.rstrip()
523
+ if line.endswith('"'):
524
+ is_multi_line = False
525
+ line = line[:-1]
526
+ # END handle quotations
527
+ optval = cursect.getlast(optname)
528
+ cursect.setlast(optname, optval + string_decode(line))
529
+ # END parse section or option
530
+ # END while reading
531
+
532
+ # If any parsing errors occurred, raise an exception.
533
+ if e:
534
+ raise e
535
+
536
+ def _has_includes(self) -> Union[bool, int]:
537
+ return self._merge_includes and len(self._included_paths())
538
+
539
+ def _included_paths(self) -> List[Tuple[str, str]]:
540
+ """List all paths that must be included to configuration.
541
+
542
+ :return:
543
+ The list of paths, where each path is a tuple of (option, value).
544
+ """
545
+ paths = []
546
+
547
+ for section in self.sections():
548
+ if section == "include":
549
+ paths += self.items(section)
550
+
551
+ match = CONDITIONAL_INCLUDE_REGEXP.search(section)
552
+ if match is None or self._repo is None:
553
+ continue
554
+
555
+ keyword = match.group(1)
556
+ value = match.group(2).strip()
557
+
558
+ if keyword in ["gitdir", "gitdir/i"]:
559
+ value = osp.expanduser(value)
560
+
561
+ if not any(value.startswith(s) for s in ["./", "/"]):
562
+ value = "**/" + value
563
+ if value.endswith("/"):
564
+ value += "**"
565
+
566
+ # Ensure that glob is always case insensitive if required.
567
+ if keyword.endswith("/i"):
568
+ value = re.sub(
569
+ r"[a-zA-Z]",
570
+ lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()),
571
+ value,
572
+ )
573
+ if self._repo.git_dir:
574
+ if fnmatch.fnmatchcase(str(self._repo.git_dir), value):
575
+ paths += self.items(section)
576
+
577
+ elif keyword == "onbranch":
578
+ try:
579
+ branch_name = self._repo.active_branch.name
580
+ except TypeError:
581
+ # Ignore section if active branch cannot be retrieved.
582
+ continue
583
+
584
+ if fnmatch.fnmatchcase(branch_name, value):
585
+ paths += self.items(section)
586
+
587
+ return paths
588
+
589
+ def read(self) -> None: # type: ignore[override]
590
+ """Read the data stored in the files we have been initialized with.
591
+
592
+ This will ignore files that cannot be read, possibly leaving an empty
593
+ configuration.
594
+
595
+ :raise IOError:
596
+ If a file cannot be handled.
597
+ """
598
+ if self._is_initialized:
599
+ return
600
+ self._is_initialized = True
601
+
602
+ files_to_read: List[Union[PathLike, IO]] = [""]
603
+ if isinstance(self._file_or_files, (str, os.PathLike)):
604
+ # For str or Path, as str is a type of Sequence.
605
+ files_to_read = [self._file_or_files]
606
+ elif not isinstance(self._file_or_files, (tuple, list, Sequence)):
607
+ # Could merge with above isinstance once runtime type known.
608
+ files_to_read = [self._file_or_files]
609
+ else: # For lists or tuples.
610
+ files_to_read = list(self._file_or_files)
611
+ # END ensure we have a copy of the paths to handle
612
+
613
+ seen = set(files_to_read)
614
+ num_read_include_files = 0
615
+ while files_to_read:
616
+ file_path = files_to_read.pop(0)
617
+ file_ok = False
618
+
619
+ if hasattr(file_path, "seek"):
620
+ # Must be a file-object.
621
+ # TODO: Replace cast with assert to narrow type, once sure.
622
+ file_path = cast(IO[bytes], file_path)
623
+ self._read(file_path, file_path.name)
624
+ else:
625
+ # Assume a path if it is not a file-object.
626
+ file_path = cast(PathLike, file_path)
627
+ try:
628
+ with open(file_path, "rb") as fp:
629
+ file_ok = True
630
+ self._read(fp, fp.name)
631
+ except IOError:
632
+ continue
633
+
634
+ # Read includes and append those that we didn't handle yet. We expect all
635
+ # paths to be normalized and absolute (and will ensure that is the case).
636
+ if self._has_includes():
637
+ for _, include_path in self._included_paths():
638
+ if include_path.startswith("~"):
639
+ include_path = osp.expanduser(include_path)
640
+ if not osp.isabs(include_path):
641
+ if not file_ok:
642
+ continue
643
+ # END ignore relative paths if we don't know the configuration file path
644
+ file_path = cast(PathLike, file_path)
645
+ assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
646
+ include_path = osp.join(osp.dirname(file_path), include_path)
647
+ # END make include path absolute
648
+ include_path = osp.normpath(include_path)
649
+ if include_path in seen or not os.access(include_path, os.R_OK):
650
+ continue
651
+ seen.add(include_path)
652
+ # Insert included file to the top to be considered first.
653
+ files_to_read.insert(0, include_path)
654
+ num_read_include_files += 1
655
+ # END each include path in configuration file
656
+ # END handle includes
657
+ # END for each file object to read
658
+
659
+ # If there was no file included, we can safely write back (potentially) the
660
+ # configuration file without altering its meaning.
661
+ if num_read_include_files == 0:
662
+ self._merge_includes = False
663
+
664
+ def _write(self, fp: IO) -> None:
665
+ """Write an .ini-format representation of the configuration state in
666
+ git compatible format."""
667
+
668
+ def write_section(name: str, section_dict: _OMD) -> None:
669
+ fp.write(("[%s]\n" % name).encode(defenc))
670
+
671
+ values: Sequence[str] # Runtime only gets str in tests, but should be whatever _OMD stores.
672
+ v: str
673
+ for key, values in section_dict.items_all():
674
+ if key == "__name__":
675
+ continue
676
+
677
+ for v in values:
678
+ fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
679
+ # END if key is not __name__
680
+
681
+ # END section writing
682
+
683
+ if self._defaults:
684
+ write_section(cp.DEFAULTSECT, self._defaults)
685
+ value: _OMD
686
+
687
+ for name, value in self._sections.items():
688
+ write_section(name, value)
689
+
690
+ def items(self, section_name: str) -> List[Tuple[str, str]]: # type: ignore[override]
691
+ """:return: list((option, value), ...) pairs of all items in the given section"""
692
+ return [(k, v) for k, v in super().items(section_name) if k != "__name__"]
693
+
694
+ def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
695
+ """:return: list((option, [values...]), ...) pairs of all items in the given section"""
696
+ rv = _OMD(self._defaults)
697
+
698
+ for k, vs in self._sections[section_name].items_all():
699
+ if k == "__name__":
700
+ continue
701
+
702
+ if k in rv and rv.getall(k) == vs:
703
+ continue
704
+
705
+ for v in vs:
706
+ rv.add(k, v)
707
+
708
+ return rv.items_all()
709
+
710
+ @needs_values
711
+ def write(self) -> None:
712
+ """Write changes to our file, if there are changes at all.
713
+
714
+ :raise IOError:
715
+ If this is a read-only writer instance or if we could not obtain a file
716
+ lock.
717
+ """
718
+ self._assure_writable("write")
719
+ if not self._dirty:
720
+ return
721
+
722
+ if isinstance(self._file_or_files, (list, tuple)):
723
+ raise AssertionError(
724
+ "Cannot write back if there is not exactly a single file to write to, have %i files"
725
+ % len(self._file_or_files)
726
+ )
727
+ # END assert multiple files
728
+
729
+ if self._has_includes():
730
+ _logger.debug(
731
+ "Skipping write-back of configuration file as include files were merged in."
732
+ + "Set merge_includes=False to prevent this."
733
+ )
734
+ return
735
+ # END stop if we have include files
736
+
737
+ fp = self._file_or_files
738
+
739
+ # We have a physical file on disk, so get a lock.
740
+ is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # TODO: Use PathLike (having dropped 3.5).
741
+ if is_file_lock and self._lock is not None: # Else raise error?
742
+ self._lock._obtain_lock()
743
+
744
+ if not hasattr(fp, "seek"):
745
+ fp = cast(PathLike, fp)
746
+ with open(fp, "wb") as fp_open:
747
+ self._write(fp_open)
748
+ else:
749
+ fp = cast("BytesIO", fp)
750
+ fp.seek(0)
751
+ # Make sure we do not overwrite into an existing file.
752
+ if hasattr(fp, "truncate"):
753
+ fp.truncate()
754
+ self._write(fp)
755
+
756
+ def _assure_writable(self, method_name: str) -> None:
757
+ if self.read_only:
758
+ raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
759
+
760
+ def add_section(self, section: str) -> None:
761
+ """Assures added options will stay in order."""
762
+ return super().add_section(section)
763
+
764
+ @property
765
+ def read_only(self) -> bool:
766
+ """:return: ``True`` if this instance may change the configuration file"""
767
+ return self._read_only
768
+
769
+ # FIXME: Figure out if default or return type can really include bool.
770
+ def get_value(
771
+ self,
772
+ section: str,
773
+ option: str,
774
+ default: Union[int, float, str, bool, None] = None,
775
+ ) -> Union[int, float, str, bool]:
776
+ """Get an option's value.
777
+
778
+ If multiple values are specified for this option in the section, the last one
779
+ specified is returned.
780
+
781
+ :param default:
782
+ If not ``None``, the given default value will be returned in case the option
783
+ did not exist.
784
+
785
+ :return:
786
+ A properly typed value, either int, float or string
787
+
788
+ :raise TypeError:
789
+ In case the value could not be understood.
790
+ Otherwise the exceptions known to the ConfigParser will be raised.
791
+ """
792
+ try:
793
+ valuestr = self.get(section, option)
794
+ except Exception:
795
+ if default is not None:
796
+ return default
797
+ raise
798
+
799
+ return self._string_to_value(valuestr)
800
+
801
+ def get_values(
802
+ self,
803
+ section: str,
804
+ option: str,
805
+ default: Union[int, float, str, bool, None] = None,
806
+ ) -> List[Union[int, float, str, bool]]:
807
+ """Get an option's values.
808
+
809
+ If multiple values are specified for this option in the section, all are
810
+ returned.
811
+
812
+ :param default:
813
+ If not ``None``, a list containing the given default value will be returned
814
+ in case the option did not exist.
815
+
816
+ :return:
817
+ A list of properly typed values, either int, float or string
818
+
819
+ :raise TypeError:
820
+ In case the value could not be understood.
821
+ Otherwise the exceptions known to the ConfigParser will be raised.
822
+ """
823
+ try:
824
+ self.sections()
825
+ lst = self._sections[section].getall(option)
826
+ except Exception:
827
+ if default is not None:
828
+ return [default]
829
+ raise
830
+
831
+ return [self._string_to_value(valuestr) for valuestr in lst]
832
+
833
+ def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]:
834
+ types = (int, float)
835
+ for numtype in types:
836
+ try:
837
+ val = numtype(valuestr)
838
+ # truncated value ?
839
+ if val != float(valuestr):
840
+ continue
841
+ return val
842
+ except (ValueError, TypeError):
843
+ continue
844
+ # END for each numeric type
845
+
846
+ # Try boolean values as git uses them.
847
+ vl = valuestr.lower()
848
+ if vl == "false":
849
+ return False
850
+ if vl == "true":
851
+ return True
852
+
853
+ if not isinstance(valuestr, str):
854
+ raise TypeError(
855
+ "Invalid value type: only int, long, float and str are allowed",
856
+ valuestr,
857
+ )
858
+
859
+ return valuestr
860
+
861
+ def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str:
862
+ if isinstance(value, (int, float, bool)):
863
+ return str(value)
864
+ return force_text(value)
865
+
866
+ @needs_values
867
+ @set_dirty_and_flush_changes
868
+ def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
869
+ """Set the given option in section to the given value.
870
+
871
+ This will create the section if required, and will not throw as opposed to the
872
+ default ConfigParser ``set`` method.
873
+
874
+ :param section:
875
+ Name of the section in which the option resides or should reside.
876
+
877
+ :param option:
878
+ Name of the options whose value to set.
879
+
880
+ :param value:
881
+ Value to set the option to. It must be a string or convertible to a string.
882
+
883
+ :return:
884
+ This instance
885
+ """
886
+ if not self.has_section(section):
887
+ self.add_section(section)
888
+ self.set(section, option, self._value_to_string(value))
889
+ return self
890
+
891
+ @needs_values
892
+ @set_dirty_and_flush_changes
893
+ def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
894
+ """Add a value for the given option in section.
895
+
896
+ This will create the section if required, and will not throw as opposed to the
897
+ default ConfigParser ``set`` method. The value becomes the new value of the
898
+ option as returned by :meth:`get_value`, and appends to the list of values
899
+ returned by :meth:`get_values`.
900
+
901
+ :param section:
902
+ Name of the section in which the option resides or should reside.
903
+
904
+ :param option:
905
+ Name of the option.
906
+
907
+ :param value:
908
+ Value to add to option. It must be a string or convertible to a string.
909
+
910
+ :return:
911
+ This instance
912
+ """
913
+ if not self.has_section(section):
914
+ self.add_section(section)
915
+ self._sections[section].add(option, self._value_to_string(value))
916
+ return self
917
+
918
+ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
919
+ """Rename the given section to `new_name`.
920
+
921
+ :raise ValueError:
922
+ If:
923
+
924
+ * `section` doesn't exist.
925
+ * A section with `new_name` does already exist.
926
+
927
+ :return:
928
+ This instance
929
+ """
930
+ if not self.has_section(section):
931
+ raise ValueError("Source section '%s' doesn't exist" % section)
932
+ if self.has_section(new_name):
933
+ raise ValueError("Destination section '%s' already exists" % new_name)
934
+
935
+ super().add_section(new_name)
936
+ new_section = self._sections[new_name]
937
+ for k, vs in self.items_all(section):
938
+ new_section.setall(k, vs)
939
+ # END for each value to copy
940
+
941
+ # This call writes back the changes, which is why we don't have the respective
942
+ # decorator.
943
+ self.remove_section(section)
944
+ return self
venv/lib/python3.10/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.10/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.10/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.10/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.10/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.10/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.10/site-packages/git/objects/__pycache__/base.cpython-310.pyc ADDED
Binary file (9.92 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/__pycache__/fun.cpython-310.pyc ADDED
Binary file (5.9 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/__pycache__/tag.cpython-310.pyc ADDED
Binary file (3.61 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/__pycache__/tree.cpython-310.pyc ADDED
Binary file (12.7 kB). View file
 
venv/lib/python3.10/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 `~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.10/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.10/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
+ `~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.10/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.10/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.10/site-packages/git/objects/submodule/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (351 Bytes). View file
 
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/base.cpython-310.pyc ADDED
Binary file (40.4 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/root.cpython-310.pyc ADDED
Binary file (9.29 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/util.cpython-310.pyc ADDED
Binary file (3.64 kB). View file
 
venv/lib/python3.10/site-packages/git/objects/submodule/base.py ADDED
@@ -0,0 +1,1632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
15
+ import git
16
+ from git.cmd import Git
17
+ from git.compat import defenc
18
+ from git.config import GitConfigParser, SectionConstraint, cp
19
+ from git.exc import (
20
+ BadName,
21
+ InvalidGitRepositoryError,
22
+ NoSuchPathError,
23
+ RepositoryDirtyError,
24
+ )
25
+ from git.objects.base import IndexObject, Object
26
+ from git.objects.util import TraversableIterableObj
27
+ from git.util import (
28
+ IterableList,
29
+ RemoteProgress,
30
+ join_path_native,
31
+ rmtree,
32
+ to_native_path_linux,
33
+ unbare_repo,
34
+ )
35
+
36
+ from .util import (
37
+ SubmoduleConfigParser,
38
+ find_first_remote_branch,
39
+ mkhead,
40
+ sm_name,
41
+ sm_section,
42
+ )
43
+
44
+ # typing ----------------------------------------------------------------------
45
+
46
+ from typing import (
47
+ Any,
48
+ Callable,
49
+ Dict,
50
+ Iterator,
51
+ Mapping,
52
+ Sequence,
53
+ TYPE_CHECKING,
54
+ Union,
55
+ cast,
56
+ )
57
+
58
+ if sys.version_info >= (3, 8):
59
+ from typing import Literal
60
+ else:
61
+ from typing_extensions import Literal
62
+
63
+ from git.types import Commit_ish, PathLike, TBD
64
+
65
+ if TYPE_CHECKING:
66
+ from git.index import IndexFile
67
+ from git.objects.commit import Commit
68
+ from git.refs import Head
69
+ from git.repo import Repo
70
+
71
+ # -----------------------------------------------------------------------------
72
+
73
+ _logger = logging.getLogger(__name__)
74
+
75
+
76
+ class UpdateProgress(RemoteProgress):
77
+ """Class providing detailed progress information to the caller who should
78
+ derive from it and implement the
79
+ :meth:`update(...) <git.util.RemoteProgress.update>` message."""
80
+
81
+ CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
82
+ _num_op_codes: int = RemoteProgress._num_op_codes + 3
83
+
84
+ __slots__ = ()
85
+
86
+
87
+ BEGIN = UpdateProgress.BEGIN
88
+ END = UpdateProgress.END
89
+ CLONE = UpdateProgress.CLONE
90
+ FETCH = UpdateProgress.FETCH
91
+ UPDWKTREE = UpdateProgress.UPDWKTREE
92
+
93
+
94
+ # IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
95
+ # mechanism, which causes plenty of trouble if the only reason for packages and modules
96
+ # is refactoring - subpackages shouldn't depend on parent packages.
97
+ class Submodule(IndexObject, TraversableIterableObj):
98
+ """Implements access to a git submodule. They are special in that their sha
99
+ represents a commit in the submodule's repository which is to be checked out
100
+ at the path of this instance.
101
+
102
+ The submodule type does not have a string type associated with it, as it exists
103
+ solely as a marker in the tree and index.
104
+
105
+ All methods work in bare and non-bare repositories.
106
+ """
107
+
108
+ _id_attribute_ = "name"
109
+ k_modules_file = ".gitmodules"
110
+ k_head_option = "branch"
111
+ k_head_default = "master"
112
+ k_default_mode = stat.S_IFDIR | stat.S_IFLNK
113
+ """Submodule flags. Submodules are directories with link-status."""
114
+
115
+ type: Literal["submodule"] = "submodule" # type: ignore[assignment]
116
+ """This is a bogus type string for base class compatibility."""
117
+
118
+ __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
119
+
120
+ _cache_attrs = ("path", "_url", "_branch_path")
121
+
122
+ def __init__(
123
+ self,
124
+ repo: "Repo",
125
+ binsha: bytes,
126
+ mode: Union[int, None] = None,
127
+ path: Union[PathLike, None] = None,
128
+ name: Union[str, None] = None,
129
+ parent_commit: Union["Commit", None] = None,
130
+ url: Union[str, None] = None,
131
+ branch_path: Union[PathLike, None] = None,
132
+ ) -> None:
133
+ """Initialize this instance with its attributes.
134
+
135
+ We only document the parameters that differ from
136
+ :class:`~git.objects.base.IndexObject`.
137
+
138
+ :param repo:
139
+ Our parent repository.
140
+
141
+ :param binsha:
142
+ Binary sha referring to a commit in the remote repository.
143
+ See the `url` parameter.
144
+
145
+ :param parent_commit:
146
+ The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
147
+ the ``.gitmodules`` blob, or ``None`` to always point to the most recent
148
+ commit. See :meth:`set_parent_commit` for details.
149
+
150
+ :param url:
151
+ The URL to the remote repository which is the submodule.
152
+
153
+ :param branch_path:
154
+ Full repository-relative path to ref to checkout when cloning the remote
155
+ repository.
156
+ """
157
+ super().__init__(repo, binsha, mode, path)
158
+ self.size = 0
159
+ self._parent_commit = parent_commit
160
+ if url is not None:
161
+ self._url = url
162
+ if branch_path is not None:
163
+ self._branch_path = branch_path
164
+ if name is not None:
165
+ self._name = name
166
+
167
+ def _set_cache_(self, attr: str) -> None:
168
+ if attr in ("path", "_url", "_branch_path"):
169
+ reader: SectionConstraint = self.config_reader()
170
+ # Default submodule values.
171
+ try:
172
+ self.path = reader.get("path")
173
+ except cp.NoSectionError as e:
174
+ if self.repo.working_tree_dir is not None:
175
+ raise ValueError(
176
+ "This submodule instance does not exist anymore in '%s' file"
177
+ % osp.join(self.repo.working_tree_dir, ".gitmodules")
178
+ ) from e
179
+
180
+ self._url = reader.get("url")
181
+ # GitPython extension values - optional.
182
+ self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
183
+ elif attr == "_name":
184
+ raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
185
+ else:
186
+ super()._set_cache_(attr)
187
+ # END handle attribute name
188
+
189
+ @classmethod
190
+ def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]:
191
+ """:return: All the submodules of our module repository"""
192
+ try:
193
+ return cls.list_items(item.module())
194
+ except InvalidGitRepositoryError:
195
+ return IterableList("")
196
+ # END handle intermediate items
197
+
198
+ @classmethod
199
+ def _need_gitfile_submodules(cls, git: Git) -> bool:
200
+ return git.version_info[:3] >= (1, 7, 5)
201
+
202
+ def __eq__(self, other: Any) -> bool:
203
+ """Compare with another submodule."""
204
+ # We may only compare by name as this should be the ID they are hashed with.
205
+ # Otherwise this type wouldn't be hashable.
206
+ # return self.path == other.path and self.url == other.url and super().__eq__(other)
207
+ return self._name == other._name
208
+
209
+ def __ne__(self, other: object) -> bool:
210
+ """Compare with another submodule for inequality."""
211
+ return not (self == other)
212
+
213
+ def __hash__(self) -> int:
214
+ """Hash this instance using its logical id, not the sha."""
215
+ return hash(self._name)
216
+
217
+ def __str__(self) -> str:
218
+ return self._name
219
+
220
+ def __repr__(self) -> str:
221
+ return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % (
222
+ type(self).__name__,
223
+ self._name,
224
+ self.path,
225
+ self.url,
226
+ self.branch_path,
227
+ )
228
+
229
+ @classmethod
230
+ def _config_parser(
231
+ cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
232
+ ) -> SubmoduleConfigParser:
233
+ """
234
+ :return:
235
+ Config parser constrained to our submodule in read or write mode
236
+
237
+ :raise IOError:
238
+ If the ``.gitmodules`` file cannot be found, either locally or in the
239
+ repository at the given parent commit. Otherwise the exception would be
240
+ delayed until the first access of the config parser.
241
+ """
242
+ parent_matches_head = True
243
+ if parent_commit is not None:
244
+ try:
245
+ parent_matches_head = repo.head.commit == parent_commit
246
+ except ValueError:
247
+ # We are most likely in an empty repository, so the HEAD doesn't point
248
+ # to a valid ref.
249
+ pass
250
+ # END handle parent_commit
251
+ fp_module: Union[str, BytesIO]
252
+ if not repo.bare and parent_matches_head and repo.working_tree_dir:
253
+ fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
254
+ else:
255
+ assert parent_commit is not None, "need valid parent_commit in bare repositories"
256
+ try:
257
+ fp_module = cls._sio_modules(parent_commit)
258
+ except KeyError as e:
259
+ raise IOError(
260
+ "Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit)
261
+ ) from e
262
+ # END handle exceptions
263
+ # END handle non-bare working tree
264
+
265
+ if not read_only and (repo.bare or not parent_matches_head):
266
+ raise ValueError("Cannot write blobs of 'historical' submodule configurations")
267
+ # END handle writes of historical submodules
268
+
269
+ return SubmoduleConfigParser(fp_module, read_only=read_only)
270
+
271
+ def _clear_cache(self) -> None:
272
+ """Clear the possibly changed values."""
273
+ for name in self._cache_attrs:
274
+ try:
275
+ delattr(self, name)
276
+ except AttributeError:
277
+ pass
278
+ # END try attr deletion
279
+ # END for each name to delete
280
+
281
+ @classmethod
282
+ def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
283
+ """
284
+ :return:
285
+ Configuration file as :class:`~io.BytesIO` - we only access it through the
286
+ respective blob's data
287
+ """
288
+ sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
289
+ sio.name = cls.k_modules_file
290
+ return sio
291
+
292
+ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
293
+ """:return: Config parser constrained to our submodule in read or write mode"""
294
+ try:
295
+ pc = self.parent_commit
296
+ except ValueError:
297
+ pc = None
298
+ # END handle empty parent repository
299
+ parser = self._config_parser(self.repo, pc, read_only)
300
+ parser.set_submodule(self)
301
+ return SectionConstraint(parser, sm_section(self.name))
302
+
303
+ @classmethod
304
+ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
305
+ if cls._need_gitfile_submodules(parent_repo.git):
306
+ return osp.join(parent_repo.git_dir, "modules", name)
307
+ if parent_repo.working_tree_dir:
308
+ return osp.join(parent_repo.working_tree_dir, path)
309
+ raise NotADirectoryError()
310
+
311
+ @classmethod
312
+ def _clone_repo(
313
+ cls,
314
+ repo: "Repo",
315
+ url: str,
316
+ path: PathLike,
317
+ name: str,
318
+ allow_unsafe_options: bool = False,
319
+ allow_unsafe_protocols: bool = False,
320
+ **kwargs: Any,
321
+ ) -> "Repo":
322
+ """
323
+ :return:
324
+ :class:`~git.repo.base.Repo` instance of newly cloned repository.
325
+
326
+ :param repo:
327
+ Our parent repository.
328
+
329
+ :param url:
330
+ URL to clone from.
331
+
332
+ :param path:
333
+ Repository-relative path to the submodule checkout location.
334
+
335
+ :param name:
336
+ Canonical name of the submodule.
337
+
338
+ :param allow_unsafe_protocols:
339
+ Allow unsafe protocols to be used, like ``ext``.
340
+
341
+ :param allow_unsafe_options:
342
+ Allow unsafe options to be used, like ``--upload-pack``.
343
+
344
+ :param kwargs:
345
+ Additional arguments given to :manpage:`git-clone(1)`.
346
+ """
347
+ module_abspath = cls._module_abspath(repo, path, name)
348
+ module_checkout_path = module_abspath
349
+ if cls._need_gitfile_submodules(repo.git):
350
+ kwargs["separate_git_dir"] = module_abspath
351
+ module_abspath_dir = osp.dirname(module_abspath)
352
+ if not osp.isdir(module_abspath_dir):
353
+ os.makedirs(module_abspath_dir)
354
+ module_checkout_path = osp.join(str(repo.working_tree_dir), path)
355
+
356
+ clone = git.Repo.clone_from(
357
+ url,
358
+ module_checkout_path,
359
+ allow_unsafe_options=allow_unsafe_options,
360
+ allow_unsafe_protocols=allow_unsafe_protocols,
361
+ **kwargs,
362
+ )
363
+ if cls._need_gitfile_submodules(repo.git):
364
+ cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
365
+
366
+ return clone
367
+
368
+ @classmethod
369
+ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
370
+ """:return: A path guaranteed to be relative to the given parent repository
371
+
372
+ :raise ValueError:
373
+ If path is not contained in the parent repository's working tree.
374
+ """
375
+ path = to_native_path_linux(path)
376
+ if path.endswith("/"):
377
+ path = path[:-1]
378
+ # END handle trailing slash
379
+
380
+ if osp.isabs(path) and parent_repo.working_tree_dir:
381
+ working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
382
+ if not path.startswith(working_tree_linux):
383
+ raise ValueError(
384
+ "Submodule checkout path '%s' needs to be within the parents repository at '%s'"
385
+ % (working_tree_linux, path)
386
+ )
387
+ path = path[len(working_tree_linux.rstrip("/")) + 1 :]
388
+ if not path:
389
+ raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
390
+ # END verify converted relative path makes sense
391
+ # END convert to a relative path
392
+
393
+ return path
394
+
395
+ @classmethod
396
+ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
397
+ """Write a ``.git`` file containing a (preferably) relative path to the actual
398
+ git module repository.
399
+
400
+ It is an error if the `module_abspath` cannot be made into a relative path,
401
+ relative to the `working_tree_dir`.
402
+
403
+ :note:
404
+ This will overwrite existing files!
405
+
406
+ :note:
407
+ As we rewrite both the git file as well as the module configuration, we
408
+ might fail on the configuration and will not roll back changes done to the
409
+ git file. This should be a non-issue, but may easily be fixed if it becomes
410
+ one.
411
+
412
+ :param working_tree_dir:
413
+ Directory to write the ``.git`` file into.
414
+
415
+ :param module_abspath:
416
+ Absolute path to the bare repository.
417
+ """
418
+ git_file = osp.join(working_tree_dir, ".git")
419
+ rela_path = osp.relpath(module_abspath, start=working_tree_dir)
420
+ if sys.platform == "win32" and osp.isfile(git_file):
421
+ os.remove(git_file)
422
+ with open(git_file, "wb") as fp:
423
+ fp.write(("gitdir: %s" % rela_path).encode(defenc))
424
+
425
+ with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer:
426
+ writer.set_value(
427
+ "core",
428
+ "worktree",
429
+ to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)),
430
+ )
431
+
432
+ # { Edit Interface
433
+
434
+ @classmethod
435
+ def add(
436
+ cls,
437
+ repo: "Repo",
438
+ name: str,
439
+ path: PathLike,
440
+ url: Union[str, None] = None,
441
+ branch: Union[str, None] = None,
442
+ no_checkout: bool = False,
443
+ depth: Union[int, None] = None,
444
+ env: Union[Mapping[str, str], None] = None,
445
+ clone_multi_options: Union[Sequence[TBD], None] = None,
446
+ allow_unsafe_options: bool = False,
447
+ allow_unsafe_protocols: bool = False,
448
+ ) -> "Submodule":
449
+ """Add a new submodule to the given repository. This will alter the index as
450
+ well as the ``.gitmodules`` file, but will not create a new commit. If the
451
+ submodule already exists, no matter if the configuration differs from the one
452
+ provided, the existing submodule will be returned.
453
+
454
+ :param repo:
455
+ Repository instance which should receive the submodule.
456
+
457
+ :param name:
458
+ The name/identifier for the submodule.
459
+
460
+ :param path:
461
+ Repository-relative or absolute path at which the submodule should be
462
+ located.
463
+ It will be created as required during the repository initialization.
464
+
465
+ :param url:
466
+ ``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more
467
+ information. If ``None``, the repository is assumed to exist, and the URL of
468
+ the first remote is taken instead. This is useful if you want to make an
469
+ existing repository a submodule of another one.
470
+
471
+ :param branch:
472
+ Name of branch at which the submodule should (later) be checked out. The
473
+ given branch must exist in the remote repository, and will be checked out
474
+ locally as a tracking branch.
475
+ It will only be written into the configuration if it not ``None``, which is
476
+ when the checked out branch will be the one the remote HEAD pointed to.
477
+ The result you get in these situation is somewhat fuzzy, and it is
478
+ recommended to specify at least ``master`` here.
479
+ Examples are ``master`` or ``feature/new``.
480
+
481
+ :param no_checkout:
482
+ If ``True``, and if the repository has to be cloned manually, no checkout
483
+ will be performed.
484
+
485
+ :param depth:
486
+ Create a shallow clone with a history truncated to the specified number of
487
+ commits.
488
+
489
+ :param env:
490
+ Optional dictionary containing the desired environment variables.
491
+
492
+ Note: Provided variables will be used to update the execution environment
493
+ for ``git``. If some variable is not specified in `env` and is defined in
494
+ attr:`os.environ`, the value from attr:`os.environ` will be used. If you
495
+ want to unset some variable, consider providing an empty string as its
496
+ value.
497
+
498
+ :param clone_multi_options:
499
+ A list of clone options. Please see
500
+ :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
501
+
502
+ :param allow_unsafe_protocols:
503
+ Allow unsafe protocols to be used, like ``ext``.
504
+
505
+ :param allow_unsafe_options:
506
+ Allow unsafe options to be used, like ``--upload-pack``.
507
+
508
+ :return:
509
+ The newly created :class:`Submodule` instance.
510
+
511
+ :note:
512
+ Works atomically, such that no change will be done if, for example, the
513
+ repository update fails.
514
+ """
515
+ if repo.bare:
516
+ raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
517
+ # END handle bare repos
518
+
519
+ path = cls._to_relative_path(repo, path)
520
+
521
+ # Ensure we never put backslashes into the URL, as might happen on Windows.
522
+ if url is not None:
523
+ url = to_native_path_linux(url)
524
+ # END ensure URL correctness
525
+
526
+ # INSTANTIATE INTERMEDIATE SM
527
+ sm = cls(
528
+ repo,
529
+ cls.NULL_BIN_SHA,
530
+ cls.k_default_mode,
531
+ path,
532
+ name,
533
+ url="invalid-temporary",
534
+ )
535
+ if sm.exists():
536
+ # Reretrieve submodule from tree.
537
+ try:
538
+ sm = repo.head.commit.tree[str(path)]
539
+ sm._name = name
540
+ return sm
541
+ except KeyError:
542
+ # Could only be in index.
543
+ index = repo.index
544
+ entry = index.entries[index.entry_key(path, 0)]
545
+ sm.binsha = entry.binsha
546
+ return sm
547
+ # END handle exceptions
548
+ # END handle existing
549
+
550
+ # fake-repo - we only need the functionality on the branch instance.
551
+ br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
552
+ has_module = sm.module_exists()
553
+ branch_is_default = branch is None
554
+ if has_module and url is not None:
555
+ if url not in [r.url for r in sm.module().remotes]:
556
+ raise ValueError(
557
+ "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath)
558
+ )
559
+ # END check url
560
+ # END verify urls match
561
+
562
+ mrepo: Union[Repo, None] = None
563
+
564
+ if url is None:
565
+ if not has_module:
566
+ raise ValueError("A URL was not given and a repository did not exist at %s" % path)
567
+ # END check url
568
+ mrepo = sm.module()
569
+ # assert isinstance(mrepo, git.Repo)
570
+ urls = [r.url for r in mrepo.remotes]
571
+ if not urls:
572
+ raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath)
573
+ # END verify we have url
574
+ url = urls[0]
575
+ else:
576
+ # Clone new repo.
577
+ kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout}
578
+ if not branch_is_default:
579
+ kwargs["b"] = br.name
580
+ # END setup checkout-branch
581
+
582
+ if depth:
583
+ if isinstance(depth, int):
584
+ kwargs["depth"] = depth
585
+ else:
586
+ raise ValueError("depth should be an integer")
587
+ if clone_multi_options:
588
+ kwargs["multi_options"] = clone_multi_options
589
+
590
+ # _clone_repo(cls, repo, url, path, name, **kwargs):
591
+ mrepo = cls._clone_repo(
592
+ repo,
593
+ url,
594
+ path,
595
+ name,
596
+ env=env,
597
+ allow_unsafe_options=allow_unsafe_options,
598
+ allow_unsafe_protocols=allow_unsafe_protocols,
599
+ **kwargs,
600
+ )
601
+ # END verify url
602
+
603
+ ## See #525 for ensuring git URLs in config-files are valid under Windows.
604
+ url = Git.polish_url(url)
605
+
606
+ # It's important to add the URL to the parent config, to let `git submodule` know.
607
+ # Otherwise there is a '-' character in front of the submodule listing:
608
+ # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
609
+ # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
610
+ writer: Union[GitConfigParser, SectionConstraint]
611
+
612
+ with sm.repo.config_writer() as writer:
613
+ writer.set_value(sm_section(name), "url", url)
614
+
615
+ # Update configuration and index.
616
+ index = sm.repo.index
617
+ with sm.config_writer(index=index, write=False) as writer:
618
+ writer.set_value("url", url)
619
+ writer.set_value("path", path)
620
+
621
+ sm._url = url
622
+ if not branch_is_default:
623
+ # Store full path.
624
+ writer.set_value(cls.k_head_option, br.path)
625
+ sm._branch_path = br.path
626
+
627
+ # We deliberately assume that our head matches our index!
628
+ if mrepo:
629
+ sm.binsha = mrepo.head.commit.binsha
630
+ index.add([sm], write=True)
631
+
632
+ return sm
633
+
634
+ def update(
635
+ self,
636
+ recursive: bool = False,
637
+ init: bool = True,
638
+ to_latest_revision: bool = False,
639
+ progress: Union["UpdateProgress", None] = None,
640
+ dry_run: bool = False,
641
+ force: bool = False,
642
+ keep_going: bool = False,
643
+ env: Union[Mapping[str, str], None] = None,
644
+ clone_multi_options: Union[Sequence[TBD], None] = None,
645
+ allow_unsafe_options: bool = False,
646
+ allow_unsafe_protocols: bool = False,
647
+ ) -> "Submodule":
648
+ """Update the repository of this submodule to point to the checkout we point at
649
+ with the binsha of this instance.
650
+
651
+ :param recursive:
652
+ If ``True``, we will operate recursively and update child modules as well.
653
+
654
+ :param init:
655
+ If ``True``, the module repository will be cloned into place if necessary.
656
+
657
+ :param to_latest_revision:
658
+ If ``True``, the submodule's sha will be ignored during checkout. Instead,
659
+ the remote will be fetched, and the local tracking branch updated. This only
660
+ works if we have a local tracking branch, which is the case if the remote
661
+ repository had a master branch, or if the ``branch`` option was specified
662
+ for this submodule and the branch existed remotely.
663
+
664
+ :param progress:
665
+ :class:`UpdateProgress` instance, or ``None`` if no progress should be
666
+ shown.
667
+
668
+ :param dry_run:
669
+ If ``True``, the operation will only be simulated, but not performed.
670
+ All performed operations are read-only.
671
+
672
+ :param force:
673
+ If ``True``, we may reset heads even if the repository in question is dirty.
674
+ Additionally we will be allowed to set a tracking branch which is ahead of
675
+ its remote branch back into the past or the location of the remote branch.
676
+ This will essentially 'forget' commits.
677
+
678
+ If ``False``, local tracking branches that are in the future of their
679
+ respective remote branches will simply not be moved.
680
+
681
+ :param keep_going:
682
+ If ``True``, we will ignore but log all errors, and keep going recursively.
683
+ Unless `dry_run` is set as well, `keep_going` could cause
684
+ subsequent/inherited errors you wouldn't see otherwise.
685
+ In conjunction with `dry_run`, it can be useful to anticipate all errors
686
+ when updating submodules.
687
+
688
+ :param env:
689
+ Optional dictionary containing the desired environment variables.
690
+
691
+ Note: Provided variables will be used to update the execution environment
692
+ for ``git``. If some variable is not specified in `env` and is defined in
693
+ attr:`os.environ`, value from attr:`os.environ` will be used.
694
+
695
+ If you want to unset some variable, consider providing the empty string as
696
+ its value.
697
+
698
+ :param clone_multi_options:
699
+ List of :manpage:`git-clone(1)` options.
700
+ Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
701
+ They only take effect with the `init` option.
702
+
703
+ :param allow_unsafe_protocols:
704
+ Allow unsafe protocols to be used, like ``ext``.
705
+
706
+ :param allow_unsafe_options:
707
+ Allow unsafe options to be used, like ``--upload-pack``.
708
+
709
+ :note:
710
+ Does nothing in bare repositories.
711
+
712
+ :note:
713
+ This method is definitely not atomic if `recursive` is ``True``.
714
+
715
+ :return:
716
+ self
717
+ """
718
+ if self.repo.bare:
719
+ return self
720
+ # END pass in bare mode
721
+
722
+ if progress is None:
723
+ progress = UpdateProgress()
724
+ # END handle progress
725
+ prefix = ""
726
+ if dry_run:
727
+ prefix = "DRY-RUN: "
728
+ # END handle prefix
729
+
730
+ # To keep things plausible in dry-run mode.
731
+ if dry_run:
732
+ mrepo = None
733
+ # END init mrepo
734
+
735
+ try:
736
+ # ENSURE REPO IS PRESENT AND UP-TO-DATE
737
+ #######################################
738
+ try:
739
+ mrepo = self.module()
740
+ rmts = mrepo.remotes
741
+ len_rmts = len(rmts)
742
+ for i, remote in enumerate(rmts):
743
+ op = FETCH
744
+ if i == 0:
745
+ op |= BEGIN
746
+ # END handle start
747
+
748
+ progress.update(
749
+ op,
750
+ i,
751
+ len_rmts,
752
+ prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
753
+ )
754
+ # ===============================
755
+ if not dry_run:
756
+ remote.fetch(progress=progress)
757
+ # END handle dry-run
758
+ # ===============================
759
+ if i == len_rmts - 1:
760
+ op |= END
761
+ # END handle end
762
+ progress.update(
763
+ op,
764
+ i,
765
+ len_rmts,
766
+ prefix + "Done fetching remote of submodule %r" % self.name,
767
+ )
768
+ # END fetch new data
769
+ except InvalidGitRepositoryError:
770
+ mrepo = None
771
+ if not init:
772
+ return self
773
+ # END early abort if init is not allowed
774
+
775
+ # There is no git-repository yet - but delete empty paths.
776
+ checkout_module_abspath = self.abspath
777
+ if not dry_run and osp.isdir(checkout_module_abspath):
778
+ try:
779
+ os.rmdir(checkout_module_abspath)
780
+ except OSError as e:
781
+ raise OSError(
782
+ "Module directory at %r does already exist and is non-empty" % checkout_module_abspath
783
+ ) from e
784
+ # END handle OSError
785
+ # END handle directory removal
786
+
787
+ # Don't check it out at first - nonetheless it will create a local
788
+ # branch according to the remote-HEAD if possible.
789
+ progress.update(
790
+ BEGIN | CLONE,
791
+ 0,
792
+ 1,
793
+ prefix
794
+ + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
795
+ )
796
+ if not dry_run:
797
+ mrepo = self._clone_repo(
798
+ self.repo,
799
+ self.url,
800
+ self.path,
801
+ self.name,
802
+ n=True,
803
+ env=env,
804
+ multi_options=clone_multi_options,
805
+ allow_unsafe_options=allow_unsafe_options,
806
+ allow_unsafe_protocols=allow_unsafe_protocols,
807
+ )
808
+ # END handle dry-run
809
+ progress.update(
810
+ END | CLONE,
811
+ 0,
812
+ 1,
813
+ prefix + "Done cloning to %s" % checkout_module_abspath,
814
+ )
815
+
816
+ if not dry_run:
817
+ # See whether we have a valid branch to check out.
818
+ try:
819
+ mrepo = cast("Repo", mrepo)
820
+ # Find a remote which has our branch - we try to be flexible.
821
+ remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
822
+ local_branch = mkhead(mrepo, self.branch_path)
823
+
824
+ # Have a valid branch, but no checkout - make sure we can figure
825
+ # that out by marking the commit with a null_sha.
826
+ local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
827
+ # END initial checkout + branch creation
828
+
829
+ # Make sure HEAD is not detached.
830
+ mrepo.head.set_reference(
831
+ local_branch,
832
+ logmsg="submodule: attaching head to %s" % local_branch,
833
+ )
834
+ mrepo.head.reference.set_tracking_branch(remote_branch)
835
+ except (IndexError, InvalidGitRepositoryError):
836
+ _logger.warning("Failed to checkout tracking branch %s", self.branch_path)
837
+ # END handle tracking branch
838
+
839
+ # NOTE: Have to write the repo config file as well, otherwise the
840
+ # default implementation will be offended and not update the
841
+ # repository. Maybe this is a good way to ensure it doesn't get into
842
+ # our way, but we want to stay backwards compatible too... It's so
843
+ # redundant!
844
+ with self.repo.config_writer() as writer:
845
+ writer.set_value(sm_section(self.name), "url", self.url)
846
+ # END handle dry_run
847
+ # END handle initialization
848
+
849
+ # DETERMINE SHAS TO CHECK OUT
850
+ #############################
851
+ binsha = self.binsha
852
+ hexsha = self.hexsha
853
+ if mrepo is not None:
854
+ # mrepo is only set if we are not in dry-run mode or if the module
855
+ # existed.
856
+ is_detached = mrepo.head.is_detached
857
+ # END handle dry_run
858
+
859
+ if mrepo is not None and to_latest_revision:
860
+ msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir
861
+ if not is_detached:
862
+ rref = mrepo.head.reference.tracking_branch()
863
+ if rref is not None:
864
+ rcommit = rref.commit
865
+ binsha = rcommit.binsha
866
+ hexsha = rcommit.hexsha
867
+ else:
868
+ _logger.error(
869
+ "%s a tracking branch was not set for local branch '%s'",
870
+ msg_base,
871
+ mrepo.head.reference,
872
+ )
873
+ # END handle remote ref
874
+ else:
875
+ _logger.error("%s there was no local tracking branch", msg_base)
876
+ # END handle detached head
877
+ # END handle to_latest_revision option
878
+
879
+ # Update the working tree.
880
+ # Handles dry_run.
881
+ if mrepo is not None and mrepo.head.commit.binsha != binsha:
882
+ # We must ensure that our destination sha (the one to point to) is in
883
+ # the future of our current head. Otherwise, we will reset changes that
884
+ # might have been done on the submodule, but were not yet pushed. We
885
+ # also handle the case that history has been rewritten, leaving no
886
+ # merge-base. In that case we behave conservatively, protecting possible
887
+ # changes the user had done.
888
+ may_reset = True
889
+ if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
890
+ base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
891
+ if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha):
892
+ if force:
893
+ msg = "Will force checkout or reset on local branch that is possibly in the future of"
894
+ msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
895
+ _logger.debug(msg)
896
+ else:
897
+ msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
898
+ msg %= (
899
+ is_detached and "checkout" or "reset",
900
+ mrepo.head,
901
+ mrepo,
902
+ )
903
+ _logger.info(msg)
904
+ may_reset = False
905
+ # END handle force
906
+ # END handle if we are in the future
907
+
908
+ if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
909
+ raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
910
+ # END handle force and dirty state
911
+ # END handle empty repo
912
+
913
+ # END verify future/past
914
+ progress.update(
915
+ BEGIN | UPDWKTREE,
916
+ 0,
917
+ 1,
918
+ prefix
919
+ + "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha),
920
+ )
921
+
922
+ if not dry_run and may_reset:
923
+ if is_detached:
924
+ # NOTE: For now we force. The user is not supposed to change
925
+ # detached submodules anyway. Maybe at some point this becomes
926
+ # an option, to properly handle user modifications - see below
927
+ # for future options regarding rebase and merge.
928
+ mrepo.git.checkout(hexsha, force=force)
929
+ else:
930
+ mrepo.head.reset(hexsha, index=True, working_tree=True)
931
+ # END handle checkout
932
+ # If we may reset/checkout.
933
+ progress.update(
934
+ END | UPDWKTREE,
935
+ 0,
936
+ 1,
937
+ prefix + "Done updating working tree for submodule %r" % self.name,
938
+ )
939
+ # END update to new commit only if needed
940
+ except Exception as err:
941
+ if not keep_going:
942
+ raise
943
+ _logger.error(str(err))
944
+ # END handle keep_going
945
+
946
+ # HANDLE RECURSION
947
+ ##################
948
+ if recursive:
949
+ # In dry_run mode, the module might not exist.
950
+ if mrepo is not None:
951
+ for submodule in self.iter_items(self.module()):
952
+ submodule.update(
953
+ recursive,
954
+ init,
955
+ to_latest_revision,
956
+ progress=progress,
957
+ dry_run=dry_run,
958
+ force=force,
959
+ keep_going=keep_going,
960
+ )
961
+ # END handle recursive update
962
+ # END handle dry run
963
+ # END for each submodule
964
+
965
+ return self
966
+
967
+ @unbare_repo
968
+ def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule":
969
+ """Move the submodule to a another module path. This involves physically moving
970
+ the repository at our current path, changing the configuration, as well as
971
+ adjusting our index entry accordingly.
972
+
973
+ :param module_path:
974
+ The path to which to move our module in the parent repository's working
975
+ tree, given as repository-relative or absolute path. Intermediate
976
+ directories will be created accordingly. If the path already exists, it must
977
+ be empty. Trailing (back)slashes are removed automatically.
978
+
979
+ :param configuration:
980
+ If ``True``, the configuration will be adjusted to let the submodule point
981
+ to the given path.
982
+
983
+ :param module:
984
+ If ``True``, the repository managed by this submodule will be moved as well.
985
+ If ``False``, we don't move the submodule's checkout, which may leave the
986
+ parent repository in an inconsistent state.
987
+
988
+ :return:
989
+ self
990
+
991
+ :raise ValueError:
992
+ If the module path existed and was not empty, or was a file.
993
+
994
+ :note:
995
+ Currently the method is not atomic, and it could leave the repository in an
996
+ inconsistent state if a sub-step fails for some reason.
997
+ """
998
+ if module + configuration < 1:
999
+ raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1000
+ # END handle input
1001
+
1002
+ module_checkout_path = self._to_relative_path(self.repo, module_path)
1003
+
1004
+ # VERIFY DESTINATION
1005
+ if module_checkout_path == self.path:
1006
+ return self
1007
+ # END handle no change
1008
+
1009
+ module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path)
1010
+ if osp.isfile(module_checkout_abspath):
1011
+ raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
1012
+ # END handle target files
1013
+
1014
+ index = self.repo.index
1015
+ tekey = index.entry_key(module_checkout_path, 0)
1016
+ # if the target item already exists, fail
1017
+ if configuration and tekey in index.entries:
1018
+ raise ValueError("Index entry for target path did already exist")
1019
+ # END handle index key already there
1020
+
1021
+ # Remove existing destination.
1022
+ if module:
1023
+ if osp.exists(module_checkout_abspath):
1024
+ if len(os.listdir(module_checkout_abspath)):
1025
+ raise ValueError("Destination module directory was not empty")
1026
+ # END handle non-emptiness
1027
+
1028
+ if osp.islink(module_checkout_abspath):
1029
+ os.remove(module_checkout_abspath)
1030
+ else:
1031
+ os.rmdir(module_checkout_abspath)
1032
+ # END handle link
1033
+ else:
1034
+ # Recreate parent directories.
1035
+ # NOTE: renames() does that now.
1036
+ pass
1037
+ # END handle existence
1038
+ # END handle module
1039
+
1040
+ # Move the module into place if possible.
1041
+ cur_path = self.abspath
1042
+ renamed_module = False
1043
+ if module and osp.exists(cur_path):
1044
+ os.renames(cur_path, module_checkout_abspath)
1045
+ renamed_module = True
1046
+
1047
+ if osp.isfile(osp.join(module_checkout_abspath, ".git")):
1048
+ module_abspath = self._module_abspath(self.repo, self.path, self.name)
1049
+ self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
1050
+ # END handle git file rewrite
1051
+ # END move physical module
1052
+
1053
+ # Rename the index entry - we have to manipulate the index directly as git-mv
1054
+ # cannot be used on submodules... yeah.
1055
+ previous_sm_path = self.path
1056
+ try:
1057
+ if configuration:
1058
+ try:
1059
+ ekey = index.entry_key(self.path, 0)
1060
+ entry = index.entries[ekey]
1061
+ del index.entries[ekey]
1062
+ nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
1063
+ index.entries[tekey] = nentry
1064
+ except KeyError as e:
1065
+ raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e
1066
+ # END handle submodule doesn't exist
1067
+
1068
+ # Update configuration.
1069
+ with self.config_writer(index=index) as writer: # Auto-write.
1070
+ writer.set_value("path", module_checkout_path)
1071
+ self.path = module_checkout_path
1072
+ # END handle configuration flag
1073
+ except Exception:
1074
+ if renamed_module:
1075
+ os.renames(module_checkout_abspath, cur_path)
1076
+ # END undo module renaming
1077
+ raise
1078
+ # END handle undo rename
1079
+
1080
+ # Auto-rename submodule if its name was 'default', that is, the checkout
1081
+ # directory.
1082
+ if previous_sm_path == self.name:
1083
+ self.rename(module_checkout_path)
1084
+
1085
+ return self
1086
+
1087
+ @unbare_repo
1088
+ def remove(
1089
+ self,
1090
+ module: bool = True,
1091
+ force: bool = False,
1092
+ configuration: bool = True,
1093
+ dry_run: bool = False,
1094
+ ) -> "Submodule":
1095
+ """Remove this submodule from the repository. This will remove our entry
1096
+ from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
1097
+
1098
+ :param module:
1099
+ If ``True``, the checked out module we point to will be deleted as well. If
1100
+ that module is currently on a commit outside any branch in the remote, or if
1101
+ it is ahead of its tracking branch, or if there are modified or untracked
1102
+ files in its working tree, then the removal will fail. In case the removal
1103
+ of the repository fails for these reasons, the submodule status will not
1104
+ have been altered.
1105
+
1106
+ If this submodule has child modules of its own, these will be deleted prior
1107
+ to touching the direct submodule.
1108
+
1109
+ :param force:
1110
+ Enforces the deletion of the module even though it contains modifications.
1111
+ This basically enforces a brute-force file system based deletion.
1112
+
1113
+ :param configuration:
1114
+ If ``True``, the submodule is deleted from the configuration, otherwise it
1115
+ isn't. Although this should be enabled most of the time, this flag enables
1116
+ you to safely delete the repository of your submodule.
1117
+
1118
+ :param dry_run:
1119
+ If ``True``, we will not actually do anything, but throw the errors we would
1120
+ usually throw.
1121
+
1122
+ :return:
1123
+ self
1124
+
1125
+ :note:
1126
+ Doesn't work in bare repositories.
1127
+
1128
+ :note:
1129
+ Doesn't work atomically, as failure to remove any part of the submodule will
1130
+ leave an inconsistent state.
1131
+
1132
+ :raise git.exc.InvalidGitRepositoryError:
1133
+ Thrown if the repository cannot be deleted.
1134
+
1135
+ :raise OSError:
1136
+ If directories or files could not be removed.
1137
+ """
1138
+ if not (module or configuration):
1139
+ raise ValueError("Need to specify to delete at least the module, or the configuration")
1140
+ # END handle parameters
1141
+
1142
+ # Recursively remove children of this submodule.
1143
+ nc = 0
1144
+ for csm in self.children():
1145
+ nc += 1
1146
+ csm.remove(module, force, configuration, dry_run)
1147
+ del csm
1148
+
1149
+ if configuration and not dry_run and nc > 0:
1150
+ # Ensure we don't leave the parent repository in a dirty state, and commit
1151
+ # our changes. It's important for recursive, unforced, deletions to work as
1152
+ # expected.
1153
+ self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
1154
+ # END handle recursion
1155
+
1156
+ # DELETE REPOSITORY WORKING TREE
1157
+ ################################
1158
+ if module and self.module_exists():
1159
+ mod = self.module()
1160
+ git_dir = mod.git_dir
1161
+ if force:
1162
+ # Take the fast lane and just delete everything in our module path.
1163
+ # TODO: If we run into permission problems, we have a highly
1164
+ # inconsistent state. Delete the .git folders last, start with the
1165
+ # submodules first.
1166
+ mp = self.abspath
1167
+ method: Union[None, Callable[[PathLike], None]] = None
1168
+ if osp.islink(mp):
1169
+ method = os.remove
1170
+ elif osp.isdir(mp):
1171
+ method = rmtree
1172
+ elif osp.exists(mp):
1173
+ raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
1174
+ # END handle brutal deletion
1175
+ if not dry_run:
1176
+ assert method
1177
+ method(mp)
1178
+ # END apply deletion method
1179
+ else:
1180
+ # Verify we may delete our module.
1181
+ if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
1182
+ raise InvalidGitRepositoryError(
1183
+ "Cannot delete module at %s with any modifications, unless force is specified"
1184
+ % mod.working_tree_dir
1185
+ )
1186
+ # END check for dirt
1187
+
1188
+ # Figure out whether we have new commits compared to the remotes.
1189
+ # NOTE: If the user pulled all the time, the remote heads might not have
1190
+ # been updated, so commits coming from the remote look as if they come
1191
+ # from us. But we stay strictly read-only and don't fetch beforehand.
1192
+ for remote in mod.remotes:
1193
+ num_branches_with_new_commits = 0
1194
+ rrefs = remote.refs
1195
+ for rref in rrefs:
1196
+ num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1197
+ # END for each remote ref
1198
+ # Not a single remote branch contained all our commits.
1199
+ if len(rrefs) and num_branches_with_new_commits == len(rrefs):
1200
+ raise InvalidGitRepositoryError(
1201
+ "Cannot delete module at %s as there are new commits" % mod.working_tree_dir
1202
+ )
1203
+ # END handle new commits
1204
+ # We have to manually delete some references to allow resources to
1205
+ # be cleaned up immediately when we are done with them, because
1206
+ # Python's scoping is no more granular than the whole function (loop
1207
+ # bodies are not scopes). When the objects stay alive longer, they
1208
+ # can keep handles open. On Windows, this is a problem.
1209
+ if len(rrefs):
1210
+ del rref # skipcq: PYL-W0631
1211
+ # END handle remotes
1212
+ del rrefs
1213
+ del remote
1214
+ # END for each remote
1215
+
1216
+ # Finally delete our own submodule.
1217
+ if not dry_run:
1218
+ self._clear_cache()
1219
+ wtd = mod.working_tree_dir
1220
+ del mod # Release file-handles (Windows).
1221
+ gc.collect()
1222
+ rmtree(str(wtd))
1223
+ # END delete tree if possible
1224
+ # END handle force
1225
+
1226
+ if not dry_run and osp.isdir(git_dir):
1227
+ self._clear_cache()
1228
+ rmtree(git_dir)
1229
+ # END handle separate bare repository
1230
+ # END handle module deletion
1231
+
1232
+ # Void our data so as not to delay invalid access.
1233
+ if not dry_run:
1234
+ self._clear_cache()
1235
+
1236
+ # DELETE CONFIGURATION
1237
+ ######################
1238
+ if configuration and not dry_run:
1239
+ # First the index-entry.
1240
+ parent_index = self.repo.index
1241
+ try:
1242
+ del parent_index.entries[parent_index.entry_key(self.path, 0)]
1243
+ except KeyError:
1244
+ pass
1245
+ # END delete entry
1246
+ parent_index.write()
1247
+
1248
+ # Now git config - we need the config intact, otherwise we can't query
1249
+ # information anymore.
1250
+
1251
+ with self.repo.config_writer() as gcp_writer:
1252
+ gcp_writer.remove_section(sm_section(self.name))
1253
+
1254
+ with self.config_writer() as sc_writer:
1255
+ sc_writer.remove_section()
1256
+ # END delete configuration
1257
+
1258
+ return self
1259
+
1260
+ def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
1261
+ """Set this instance to use the given commit whose tree is supposed to
1262
+ contain the ``.gitmodules`` blob.
1263
+
1264
+ :param commit:
1265
+ Commit-ish reference pointing at the root tree, or ``None`` to always point
1266
+ to the most recent commit.
1267
+
1268
+ :param check:
1269
+ If ``True``, relatively expensive checks will be performed to verify
1270
+ validity of the submodule.
1271
+
1272
+ :raise ValueError:
1273
+ If the commit's tree didn't contain the ``.gitmodules`` blob.
1274
+
1275
+ :raise ValueError:
1276
+ If the parent commit didn't store this submodule under the current path.
1277
+
1278
+ :return:
1279
+ self
1280
+ """
1281
+ if commit is None:
1282
+ self._parent_commit = None
1283
+ return self
1284
+ # END handle None
1285
+ pcommit = self.repo.commit(commit)
1286
+ pctree = pcommit.tree
1287
+ if self.k_modules_file not in pctree:
1288
+ raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file))
1289
+ # END handle exceptions
1290
+
1291
+ prev_pc = self._parent_commit
1292
+ self._parent_commit = pcommit
1293
+
1294
+ if check:
1295
+ parser = self._config_parser(self.repo, self._parent_commit, read_only=True)
1296
+ if not parser.has_section(sm_section(self.name)):
1297
+ self._parent_commit = prev_pc
1298
+ raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit))
1299
+ # END handle submodule did not exist
1300
+ # END handle checking mode
1301
+
1302
+ # Update our sha, it could have changed.
1303
+ # If check is False, we might see a parent-commit that doesn't even contain the
1304
+ # submodule anymore. in that case, mark our sha as being NULL.
1305
+ try:
1306
+ self.binsha = pctree[str(self.path)].binsha
1307
+ except KeyError:
1308
+ self.binsha = self.NULL_BIN_SHA
1309
+
1310
+ self._clear_cache()
1311
+ return self
1312
+
1313
+ @unbare_repo
1314
+ def config_writer(
1315
+ self, index: Union["IndexFile", None] = None, write: bool = True
1316
+ ) -> SectionConstraint["SubmoduleConfigParser"]:
1317
+ """
1318
+ :return:
1319
+ A config writer instance allowing you to read and write the data belonging
1320
+ to this submodule into the ``.gitmodules`` file.
1321
+
1322
+ :param index:
1323
+ If not ``None``, an :class:`~git.index.base.IndexFile` instance which should
1324
+ be written. Defaults to the index of the :class:`Submodule`'s parent
1325
+ repository.
1326
+
1327
+ :param write:
1328
+ If ``True``, the index will be written each time a configuration value changes.
1329
+
1330
+ :note:
1331
+ The parameters allow for a more efficient writing of the index, as you can
1332
+ pass in a modified index on your own, prevent automatic writing, and write
1333
+ yourself once the whole operation is complete.
1334
+
1335
+ :raise ValueError:
1336
+ If trying to get a writer on a parent_commit which does not match the
1337
+ current head commit.
1338
+
1339
+ :raise IOError:
1340
+ If the ``.gitmodules`` file/blob could not be read.
1341
+ """
1342
+ writer = self._config_parser_constrained(read_only=False)
1343
+ if index is not None:
1344
+ writer.config._index = index
1345
+ writer.config._auto_write = write
1346
+ return writer
1347
+
1348
+ @unbare_repo
1349
+ def rename(self, new_name: str) -> "Submodule":
1350
+ """Rename this submodule.
1351
+
1352
+ :note:
1353
+ This method takes care of renaming the submodule in various places, such as:
1354
+
1355
+ * ``$parent_git_dir / config``
1356
+ * ``$working_tree_dir / .gitmodules``
1357
+ * (git >= v1.8.0: move submodule repository to new name)
1358
+
1359
+ As ``.gitmodules`` will be changed, you would need to make a commit afterwards.
1360
+ The changed ``.gitmodules`` file will already be added to the index.
1361
+
1362
+ :return:
1363
+ This :class:`Submodule` instance
1364
+ """
1365
+ if self.name == new_name:
1366
+ return self
1367
+
1368
+ # .git/config
1369
+ with self.repo.config_writer() as pw:
1370
+ # As we ourselves didn't write anything about submodules into the parent
1371
+ # .git/config, we will not require it to exist, and just ignore missing
1372
+ # entries.
1373
+ if pw.has_section(sm_section(self.name)):
1374
+ pw.rename_section(sm_section(self.name), sm_section(new_name))
1375
+
1376
+ # .gitmodules
1377
+ with self.config_writer(write=True).config as cw:
1378
+ cw.rename_section(sm_section(self.name), sm_section(new_name))
1379
+
1380
+ self._name = new_name
1381
+
1382
+ # .git/modules
1383
+ mod = self.module()
1384
+ if mod.has_separate_working_tree():
1385
+ destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
1386
+ source_dir = mod.git_dir
1387
+ # Let's be sure the submodule name is not so obviously tied to a directory.
1388
+ if str(destination_module_abspath).startswith(str(mod.git_dir)):
1389
+ tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
1390
+ os.renames(source_dir, tmp_dir)
1391
+ source_dir = tmp_dir
1392
+ # END handle self-containment
1393
+ os.renames(source_dir, destination_module_abspath)
1394
+ if mod.working_tree_dir:
1395
+ self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
1396
+ # END move separate git repository
1397
+
1398
+ return self
1399
+
1400
+ # } END edit interface
1401
+
1402
+ # { Query Interface
1403
+
1404
+ @unbare_repo
1405
+ def module(self) -> "Repo":
1406
+ """
1407
+ :return:
1408
+ :class:`~git.repo.base.Repo` instance initialized from the repository at our
1409
+ submodule path
1410
+
1411
+ :raise git.exc.InvalidGitRepositoryError:
1412
+ If a repository was not available.
1413
+ This could also mean that it was not yet initialized.
1414
+ """
1415
+ module_checkout_abspath = self.abspath
1416
+ try:
1417
+ repo = git.Repo(module_checkout_abspath)
1418
+ if repo != self.repo:
1419
+ return repo
1420
+ # END handle repo uninitialized
1421
+ except (InvalidGitRepositoryError, NoSuchPathError) as e:
1422
+ raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e
1423
+ else:
1424
+ raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath)
1425
+ # END handle exceptions
1426
+
1427
+ def module_exists(self) -> bool:
1428
+ """
1429
+ :return:
1430
+ ``True`` if our module exists and is a valid git repository.
1431
+ See the :meth:`module` method.
1432
+ """
1433
+ try:
1434
+ self.module()
1435
+ return True
1436
+ except Exception:
1437
+ return False
1438
+ # END handle exception
1439
+
1440
+ def exists(self) -> bool:
1441
+ """
1442
+ :return:
1443
+ ``True`` if the submodule exists, ``False`` otherwise.
1444
+ Please note that a submodule may exist (in the ``.gitmodules`` file) even
1445
+ though its module doesn't exist on disk.
1446
+ """
1447
+ # Keep attributes for later, and restore them if we have no valid data.
1448
+ # This way we do not actually alter the state of the object.
1449
+ loc = locals()
1450
+ for attr in self._cache_attrs:
1451
+ try:
1452
+ if hasattr(self, attr):
1453
+ loc[attr] = getattr(self, attr)
1454
+ # END if we have the attribute cache
1455
+ except (cp.NoSectionError, ValueError):
1456
+ # On PY3, this can happen apparently... don't know why this doesn't
1457
+ # happen on PY2.
1458
+ pass
1459
+ # END for each attr
1460
+ self._clear_cache()
1461
+
1462
+ try:
1463
+ try:
1464
+ self.path # noqa: B018
1465
+ return True
1466
+ except Exception:
1467
+ return False
1468
+ # END handle exceptions
1469
+ finally:
1470
+ for attr in self._cache_attrs:
1471
+ if attr in loc:
1472
+ setattr(self, attr, loc[attr])
1473
+ # END if we have a cache
1474
+ # END reapply each attribute
1475
+ # END handle object state consistency
1476
+
1477
+ @property
1478
+ def branch(self) -> "Head":
1479
+ """
1480
+ :return:
1481
+ The branch instance that we are to checkout
1482
+
1483
+ :raise git.exc.InvalidGitRepositoryError:
1484
+ If our module is not yet checked out.
1485
+ """
1486
+ return mkhead(self.module(), self._branch_path)
1487
+
1488
+ @property
1489
+ def branch_path(self) -> PathLike:
1490
+ """
1491
+ :return:
1492
+ Full repository-relative path as string to the branch we would checkout from
1493
+ the remote and track
1494
+ """
1495
+ return self._branch_path
1496
+
1497
+ @property
1498
+ def branch_name(self) -> str:
1499
+ """
1500
+ :return:
1501
+ The name of the branch, which is the shortest possible branch name
1502
+ """
1503
+ # Use an instance method, for this we create a temporary Head instance which
1504
+ # uses a repository that is available at least (it makes no difference).
1505
+ return git.Head(self.repo, self._branch_path).name
1506
+
1507
+ @property
1508
+ def url(self) -> str:
1509
+ """:return: The url to the repository our submodule's repository refers to"""
1510
+ return self._url
1511
+
1512
+ @property
1513
+ def parent_commit(self) -> "Commit":
1514
+ """
1515
+ :return:
1516
+ :class:`~git.objects.commit.Commit` instance with the tree containing the
1517
+ ``.gitmodules`` file
1518
+
1519
+ :note:
1520
+ Will always point to the current head's commit if it was not set explicitly.
1521
+ """
1522
+ if self._parent_commit is None:
1523
+ return self.repo.commit()
1524
+ return self._parent_commit
1525
+
1526
+ @property
1527
+ def name(self) -> str:
1528
+ """
1529
+ :return:
1530
+ The name of this submodule. It is used to identify it within the
1531
+ ``.gitmodules`` file.
1532
+
1533
+ :note:
1534
+ By default, this is the name is the path at which to find the submodule, but
1535
+ in GitPython it should be a unique identifier similar to the identifiers
1536
+ used for remotes, which allows to change the path of the submodule easily.
1537
+ """
1538
+ return self._name
1539
+
1540
+ def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
1541
+ """
1542
+ :return:
1543
+ ConfigReader instance which allows you to query the configuration values of
1544
+ this submodule, as provided by the ``.gitmodules`` file.
1545
+
1546
+ :note:
1547
+ The config reader will actually read the data directly from the repository
1548
+ and thus does not need nor care about your working tree.
1549
+
1550
+ :note:
1551
+ Should be cached by the caller and only kept as long as needed.
1552
+
1553
+ :raise IOError:
1554
+ If the ``.gitmodules`` file/blob could not be read.
1555
+ """
1556
+ return self._config_parser_constrained(read_only=True)
1557
+
1558
+ def children(self) -> IterableList["Submodule"]:
1559
+ """
1560
+ :return:
1561
+ IterableList(Submodule, ...) An iterable list of :class:`Submodule`
1562
+ instances which are children of this submodule or 0 if the submodule is not
1563
+ checked out.
1564
+ """
1565
+ return self._get_intermediate_items(self)
1566
+
1567
+ # } END query interface
1568
+
1569
+ # { Iterable Interface
1570
+
1571
+ @classmethod
1572
+ def iter_items(
1573
+ cls,
1574
+ repo: "Repo",
1575
+ parent_commit: Union[Commit_ish, str] = "HEAD",
1576
+ *args: Any,
1577
+ **kwargs: Any,
1578
+ ) -> Iterator["Submodule"]:
1579
+ """
1580
+ :return:
1581
+ Iterator yielding :class:`Submodule` instances available in the given
1582
+ repository
1583
+ """
1584
+ try:
1585
+ pc = repo.commit(parent_commit) # Parent commit instance
1586
+ parser = cls._config_parser(repo, pc, read_only=True)
1587
+ except (IOError, BadName):
1588
+ return
1589
+ # END handle empty iterator
1590
+
1591
+ for sms in parser.sections():
1592
+ n = sm_name(sms)
1593
+ p = parser.get(sms, "path")
1594
+ u = parser.get(sms, "url")
1595
+ b = cls.k_head_default
1596
+ if parser.has_option(sms, cls.k_head_option):
1597
+ b = str(parser.get(sms, cls.k_head_option))
1598
+ # END handle optional information
1599
+
1600
+ # Get the binsha.
1601
+ index = repo.index
1602
+ try:
1603
+ rt = pc.tree # Root tree
1604
+ sm = rt[p]
1605
+ except KeyError:
1606
+ # Try the index, maybe it was just added.
1607
+ try:
1608
+ entry = index.entries[index.entry_key(p, 0)]
1609
+ sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
1610
+ except KeyError:
1611
+ # The submodule doesn't exist, probably it wasn't removed from the
1612
+ # .gitmodules file.
1613
+ continue
1614
+ # END handle keyerror
1615
+ # END handle critical error
1616
+
1617
+ # Make sure we are looking at a submodule object.
1618
+ if type(sm) is not git.objects.submodule.base.Submodule:
1619
+ continue
1620
+
1621
+ # Fill in remaining info - saves time as it doesn't have to be parsed again.
1622
+ sm._name = n
1623
+ if pc != repo.commit():
1624
+ sm._parent_commit = pc
1625
+ # END set only if not most recent!
1626
+ sm._branch_path = git.Head.to_full_path(b)
1627
+ sm._url = u
1628
+
1629
+ yield sm
1630
+ # END for each section
1631
+
1632
+ # } END iterable interface
venv/lib/python3.10/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.10/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.10/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.10/site-packages/git/objects/tree.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b)
54
+
55
+
56
+ class TreeModifier:
57
+ """A utility class providing methods to alter the underlying cache in a list-like
58
+ fashion.
59
+
60
+ Once all adjustments are complete, the :attr:`_cache`, which really is a reference
61
+ to the cache of a tree, will be sorted. This ensures it will be in a serializable
62
+ state.
63
+ """
64
+
65
+ __slots__ = ("_cache",)
66
+
67
+ def __init__(self, cache: List[TreeCacheTup]) -> None:
68
+ self._cache = cache
69
+
70
+ def _index_by_name(self, name: str) -> int:
71
+ """:return: index of an item with name, or -1 if not found"""
72
+ for i, t in enumerate(self._cache):
73
+ if t[2] == name:
74
+ return i
75
+ # END found item
76
+ # END for each item in cache
77
+ return -1
78
+
79
+ # { Interface
80
+ def set_done(self) -> "TreeModifier":
81
+ """Call this method once you are done modifying the tree information.
82
+
83
+ This may be called several times, but be aware that each call will cause a sort
84
+ operation.
85
+
86
+ :return:
87
+ self
88
+ """
89
+ self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
90
+ return self
91
+
92
+ # } END interface
93
+
94
+ # { Mutators
95
+ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeModifier":
96
+ """Add the given item to the tree.
97
+
98
+ If an item with the given name already exists, nothing will be done, but a
99
+ :exc:`ValueError` will be raised if the sha and mode of the existing item do not
100
+ match the one you add, unless `force` is ``True``.
101
+
102
+ :param sha:
103
+ The 20 or 40 byte sha of the item to add.
104
+
105
+ :param mode:
106
+ :class:`int` representing the stat-compatible mode of the item.
107
+
108
+ :param force:
109
+ If ``True``, an item with your name and information will overwrite any
110
+ existing item with the same name, no matter which information it has.
111
+
112
+ :return:
113
+ self
114
+ """
115
+ if "/" in name:
116
+ raise ValueError("Name must not contain '/' characters")
117
+ if (mode >> 12) not in Tree._map_id_to_type:
118
+ raise ValueError("Invalid object type according to mode %o" % mode)
119
+
120
+ sha = to_bin_sha(sha)
121
+ index = self._index_by_name(name)
122
+
123
+ item = (sha, mode, name)
124
+
125
+ if index == -1:
126
+ self._cache.append(item)
127
+ else:
128
+ if force:
129
+ self._cache[index] = item
130
+ else:
131
+ ex_item = self._cache[index]
132
+ if ex_item[0] != sha or ex_item[1] != mode:
133
+ raise ValueError("Item %r existed with different properties" % name)
134
+ # END handle mismatch
135
+ # END handle force
136
+ # END handle name exists
137
+ return self
138
+
139
+ def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
140
+ """Add the given item to the tree. Its correctness is assumed, so it is the
141
+ caller's responsibility to ensure that the input is correct.
142
+
143
+ For more information on the parameters, see :meth:`add`.
144
+
145
+ :param binsha:
146
+ 20 byte binary sha.
147
+ """
148
+ assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str)
149
+ tree_cache = (binsha, mode, name)
150
+
151
+ self._cache.append(tree_cache)
152
+
153
+ def __delitem__(self, name: str) -> None:
154
+ """Delete an item with the given name if it exists."""
155
+ index = self._index_by_name(name)
156
+ if index > -1:
157
+ del self._cache[index]
158
+
159
+ # } END mutators
160
+
161
+
162
+ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
163
+ R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
164
+ other :class:`Tree`\s.
165
+
166
+ See :manpage:`gitglossary(7)` on "tree object":
167
+ https://git-scm.com/docs/gitglossary#def_tree_object
168
+
169
+ Subscripting is supported, as with a list or dict:
170
+
171
+ * Access a specific blob using the ``tree["filename"]`` notation.
172
+ * You may likewise access by index, like ``blob = tree[0]``.
173
+ """
174
+
175
+ type: Literal["tree"] = "tree"
176
+
177
+ __slots__ = ("_cache",)
178
+
179
+ # Actual integer IDs for comparison.
180
+ commit_id = 0o16 # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link.
181
+ blob_id = 0o10
182
+ symlink_id = 0o12
183
+ tree_id = 0o04
184
+
185
+ _map_id_to_type: Dict[int, Type[IndexObjUnion]] = {
186
+ commit_id: Submodule,
187
+ blob_id: Blob,
188
+ symlink_id: Blob,
189
+ # Tree ID added once Tree is defined.
190
+ }
191
+
192
+ def __init__(
193
+ self,
194
+ repo: "Repo",
195
+ binsha: bytes,
196
+ mode: int = tree_id << 12,
197
+ path: Union[PathLike, None] = None,
198
+ ):
199
+ super().__init__(repo, binsha, mode, path)
200
+
201
+ @classmethod
202
+ def _get_intermediate_items(
203
+ cls,
204
+ index_object: IndexObjUnion,
205
+ ) -> Union[Tuple["Tree", ...], Tuple[()]]:
206
+ if index_object.type == "tree":
207
+ return tuple(index_object._iter_convert_to_object(index_object._cache))
208
+ return ()
209
+
210
+ def _set_cache_(self, attr: str) -> None:
211
+ if attr == "_cache":
212
+ # Set the data when we need it.
213
+ ostream = self.repo.odb.stream(self.binsha)
214
+ self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read())
215
+ else:
216
+ super()._set_cache_(attr)
217
+ # END handle attribute
218
+
219
+ def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
220
+ """Iterable yields tuples of (binsha, mode, name), which will be converted to
221
+ the respective object representation.
222
+ """
223
+ for binsha, mode, name in iterable:
224
+ path = join_path(self.path, name)
225
+ try:
226
+ yield self._map_id_to_type[mode >> 12](self.repo, binsha, mode, path)
227
+ except KeyError as e:
228
+ raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e
229
+ # END for each item
230
+
231
+ def join(self, file: str) -> IndexObjUnion:
232
+ """Find the named object in this tree's contents.
233
+
234
+ :return:
235
+ :class:`~git.objects.blob.Blob`, :class:`Tree`, or
236
+ :class:`~git.objects.submodule.base.Submodule`
237
+
238
+ :raise KeyError:
239
+ If the given file or tree does not exist in this tree.
240
+ """
241
+ msg = "Blob or Tree named %r not found"
242
+ if "/" in file:
243
+ tree = self
244
+ item = self
245
+ tokens = file.split("/")
246
+ for i, token in enumerate(tokens):
247
+ item = tree[token]
248
+ if item.type == "tree":
249
+ tree = item
250
+ else:
251
+ # Safety assertion - blobs are at the end of the path.
252
+ if i != len(tokens) - 1:
253
+ raise KeyError(msg % file)
254
+ return item
255
+ # END handle item type
256
+ # END for each token of split path
257
+ if item == self:
258
+ raise KeyError(msg % file)
259
+ return item
260
+ else:
261
+ for info in self._cache:
262
+ if info[2] == file: # [2] == name
263
+ return self._map_id_to_type[info[1] >> 12](
264
+ self.repo, info[0], info[1], join_path(self.path, info[2])
265
+ )
266
+ # END for each obj
267
+ raise KeyError(msg % file)
268
+ # END handle long paths
269
+
270
+ def __truediv__(self, file: str) -> IndexObjUnion:
271
+ """The ``/`` operator is another syntax for joining.
272
+
273
+ See :meth:`join` for details.
274
+ """
275
+ return self.join(file)
276
+
277
+ @property
278
+ def trees(self) -> List["Tree"]:
279
+ """:return: list(Tree, ...) List of trees directly below this tree"""
280
+ return [i for i in self if i.type == "tree"]
281
+
282
+ @property
283
+ def blobs(self) -> List[Blob]:
284
+ """:return: list(Blob, ...) List of blobs directly below this tree"""
285
+ return [i for i in self if i.type == "blob"]
286
+
287
+ @property
288
+ def cache(self) -> TreeModifier:
289
+ """
290
+ :return:
291
+ An object allowing modification of the internal cache. This can be used to
292
+ change the tree's contents. When done, make sure you call
293
+ :meth:`~TreeModifier.set_done` on the tree modifier, or serialization
294
+ behaviour will be incorrect.
295
+
296
+ :note:
297
+ See :class:`TreeModifier` for more information on how to alter the cache.
298
+ """
299
+ return TreeModifier(self._cache)
300
+
301
+ def traverse(
302
+ self,
303
+ predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True,
304
+ prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False,
305
+ depth: int = -1,
306
+ branch_first: bool = True,
307
+ visit_once: bool = False,
308
+ ignore_self: int = 1,
309
+ as_edge: bool = False,
310
+ ) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]:
311
+ """For documentation, see
312
+ `Traversable._traverse() <git.objects.util.Traversable._traverse>`.
313
+
314
+ Trees are set to ``visit_once = False`` to gain more performance in the
315
+ traversal.
316
+ """
317
+
318
+ # # To typecheck instead of using cast.
319
+ # import itertools
320
+ # def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]:
321
+ # return all(isinstance(x, (Blob, Tree, Submodule)) for x in inp[1])
322
+
323
+ # ret = super().traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
324
+ # ret_tup = itertools.tee(ret, 2)
325
+ # assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}"
326
+ # return ret_tup[0]
327
+
328
+ return cast(
329
+ Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
330
+ super()._traverse(
331
+ predicate, # type: ignore[arg-type]
332
+ prune, # type: ignore[arg-type]
333
+ depth,
334
+ branch_first,
335
+ visit_once,
336
+ ignore_self,
337
+ ),
338
+ )
339
+
340
+ def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion]:
341
+ """
342
+ :return:
343
+ :class:`~git.util.IterableList` with the results of the traversal as
344
+ produced by :meth:`traverse`
345
+
346
+ Tree -> IterableList[Union[Submodule, Tree, Blob]]
347
+ """
348
+ return super()._list_traverse(*args, **kwargs)
349
+
350
+ # List protocol
351
+
352
+ def __getslice__(self, i: int, j: int) -> List[IndexObjUnion]:
353
+ return list(self._iter_convert_to_object(self._cache[i:j]))
354
+
355
+ def __iter__(self) -> Iterator[IndexObjUnion]:
356
+ return self._iter_convert_to_object(self._cache)
357
+
358
+ def __len__(self) -> int:
359
+ return len(self._cache)
360
+
361
+ def __getitem__(self, item: Union[str, int, slice]) -> IndexObjUnion:
362
+ if isinstance(item, int):
363
+ info = self._cache[item]
364
+ return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2]))
365
+
366
+ if isinstance(item, str):
367
+ # compatibility
368
+ return self.join(item)
369
+ # END index is basestring
370
+
371
+ raise TypeError("Invalid index type: %r" % item)
372
+
373
+ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
374
+ if isinstance(item, IndexObject):
375
+ for info in self._cache:
376
+ if item.binsha == info[0]:
377
+ return True
378
+ # END compare sha
379
+ # END for each entry
380
+ # END handle item is index object
381
+ # compatibility
382
+
383
+ # Treat item as repo-relative path.
384
+ else:
385
+ path = self.path
386
+ for info in self._cache:
387
+ if item == join_path(path, info[2]):
388
+ return True
389
+ # END for each item
390
+ return False
391
+
392
+ def __reversed__(self) -> Iterator[IndexObjUnion]:
393
+ return reversed(self._iter_convert_to_object(self._cache)) # type: ignore[call-overload]
394
+
395
+ def _serialize(self, stream: "BytesIO") -> "Tree":
396
+ """Serialize this tree into the stream. Assumes sorted tree data.
397
+
398
+ :note:
399
+ We will assume our tree data to be in a sorted state. If this is not the
400
+ case, serialization will not generate a correct tree representation as these
401
+ are assumed to be sorted by algorithms.
402
+ """
403
+ tree_to_stream(self._cache, stream.write)
404
+ return self
405
+
406
+ def _deserialize(self, stream: "BytesIO") -> "Tree":
407
+ self._cache = tree_entries_from_data(stream.read())
408
+ return self
409
+
410
+
411
+ # END tree
412
+
413
+ # Finalize map definition.
414
+ Tree._map_id_to_type[Tree.tree_id] = Tree
venv/lib/python3.10/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
+ )