TheDavidYoungblood commited on
Commit
73d2546
1 Parent(s): d949a7a

Init-Commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/AUTHORS +58 -0
  2. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/INSTALLER +1 -0
  3. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/LICENSE +29 -0
  4. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/METADATA +297 -0
  5. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/RECORD +83 -0
  6. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/REQUESTED +0 -0
  7. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/WHEEL +5 -0
  8. ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/top_level.txt +1 -0
  9. ILYA/Lib/site-packages/_distutils_hack/__init__.py +222 -0
  10. ILYA/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc +0 -0
  11. ILYA/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc +0 -0
  12. ILYA/Lib/site-packages/_distutils_hack/override.py +1 -0
  13. ILYA/Lib/site-packages/distutils-precedence.pth +3 -0
  14. ILYA/Lib/site-packages/git/__init__.py +300 -0
  15. ILYA/Lib/site-packages/git/__pycache__/__init__.cpython-311.pyc +0 -0
  16. ILYA/Lib/site-packages/git/__pycache__/cmd.cpython-311.pyc +0 -0
  17. ILYA/Lib/site-packages/git/__pycache__/compat.cpython-311.pyc +0 -0
  18. ILYA/Lib/site-packages/git/__pycache__/config.cpython-311.pyc +0 -0
  19. ILYA/Lib/site-packages/git/__pycache__/db.cpython-311.pyc +0 -0
  20. ILYA/Lib/site-packages/git/__pycache__/diff.cpython-311.pyc +0 -0
  21. ILYA/Lib/site-packages/git/__pycache__/exc.cpython-311.pyc +0 -0
  22. ILYA/Lib/site-packages/git/__pycache__/remote.cpython-311.pyc +0 -0
  23. ILYA/Lib/site-packages/git/__pycache__/types.cpython-311.pyc +0 -0
  24. ILYA/Lib/site-packages/git/__pycache__/util.cpython-311.pyc +0 -0
  25. ILYA/Lib/site-packages/git/cmd.py +1723 -0
  26. ILYA/Lib/site-packages/git/compat.py +165 -0
  27. ILYA/Lib/site-packages/git/config.py +944 -0
  28. ILYA/Lib/site-packages/git/db.py +71 -0
  29. ILYA/Lib/site-packages/git/diff.py +775 -0
  30. ILYA/Lib/site-packages/git/exc.py +228 -0
  31. ILYA/Lib/site-packages/git/index/__init__.py +16 -0
  32. ILYA/Lib/site-packages/git/index/__pycache__/__init__.cpython-311.pyc +0 -0
  33. ILYA/Lib/site-packages/git/index/__pycache__/base.cpython-311.pyc +0 -0
  34. ILYA/Lib/site-packages/git/index/__pycache__/fun.cpython-311.pyc +0 -0
  35. ILYA/Lib/site-packages/git/index/__pycache__/typ.cpython-311.pyc +0 -0
  36. ILYA/Lib/site-packages/git/index/__pycache__/util.cpython-311.pyc +0 -0
  37. ILYA/Lib/site-packages/git/index/base.py +1518 -0
  38. ILYA/Lib/site-packages/git/index/fun.py +465 -0
  39. ILYA/Lib/site-packages/git/index/typ.py +202 -0
  40. ILYA/Lib/site-packages/git/index/util.py +121 -0
  41. ILYA/Lib/site-packages/git/objects/__init__.py +25 -0
  42. ILYA/Lib/site-packages/git/objects/__pycache__/__init__.cpython-311.pyc +0 -0
  43. ILYA/Lib/site-packages/git/objects/__pycache__/base.cpython-311.pyc +0 -0
  44. ILYA/Lib/site-packages/git/objects/__pycache__/blob.cpython-311.pyc +0 -0
  45. ILYA/Lib/site-packages/git/objects/__pycache__/commit.cpython-311.pyc +0 -0
  46. ILYA/Lib/site-packages/git/objects/__pycache__/fun.cpython-311.pyc +0 -0
  47. ILYA/Lib/site-packages/git/objects/__pycache__/tag.cpython-311.pyc +0 -0
  48. ILYA/Lib/site-packages/git/objects/__pycache__/tree.cpython-311.pyc +0 -0
  49. ILYA/Lib/site-packages/git/objects/__pycache__/util.cpython-311.pyc +0 -0
  50. ILYA/Lib/site-packages/git/objects/base.py +301 -0
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/AUTHORS ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 <[email protected]>
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 <[email protected]>
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
+
58
+ Portions derived from other open source works and are clearly marked.
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
ILYA/Lib/site-packages/GitPython-3.1.43.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.
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/METADATA ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: GitPython
3
+ Version: 3.1.43
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
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: doc
33
+ Requires-Dist: sphinx ==4.3.2 ; extra == 'doc'
34
+ Requires-Dist: sphinx-rtd-theme ; extra == 'doc'
35
+ Requires-Dist: sphinxcontrib-applehelp <=1.0.4,>=1.0.2 ; extra == 'doc'
36
+ Requires-Dist: sphinxcontrib-devhelp ==1.0.2 ; extra == 'doc'
37
+ Requires-Dist: sphinxcontrib-htmlhelp <=2.0.1,>=2.0.0 ; extra == 'doc'
38
+ Requires-Dist: sphinxcontrib-qthelp ==1.0.3 ; extra == 'doc'
39
+ Requires-Dist: sphinxcontrib-serializinghtml ==1.1.5 ; extra == 'doc'
40
+ Requires-Dist: sphinx-autodoc-typehints ; extra == 'doc'
41
+ Provides-Extra: test
42
+ Requires-Dist: coverage[toml] ; extra == 'test'
43
+ Requires-Dist: ddt !=1.4.3,>=1.1.1 ; extra == 'test'
44
+ Requires-Dist: mypy ; extra == 'test'
45
+ Requires-Dist: pre-commit ; extra == 'test'
46
+ Requires-Dist: pytest >=7.3.1 ; extra == 'test'
47
+ Requires-Dist: pytest-cov ; extra == 'test'
48
+ Requires-Dist: pytest-instafail ; extra == 'test'
49
+ Requires-Dist: pytest-mock ; extra == 'test'
50
+ Requires-Dist: pytest-sugar ; extra == 'test'
51
+ Requires-Dist: typing-extensions ; (python_version < "3.11") and extra == 'test'
52
+ Requires-Dist: mock ; (python_version < "3.8") and extra == 'test'
53
+
54
+ ![Python package](https://github.com/gitpython-developers/GitPython/workflows/Python%20package/badge.svg)
55
+ [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
56
+ [![Packaging status](https://repology.org/badge/tiny-repos/python:gitpython.svg)](https://repology.org/metapackage/python:gitpython/versions)
57
+
58
+ ## [Gitoxide](https://github.com/Byron/gitoxide): A peek into the future…
59
+
60
+ I started working on GitPython in 2009, back in the days when Python was 'my thing' and I had great plans with it.
61
+ Of course, back in the days, I didn't really know what I was doing and this shows in many places. Somewhat similar to
62
+ Python this happens to be 'good enough', but at the same time is deeply flawed and broken beyond repair.
63
+
64
+ 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.
65
+ 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.
66
+
67
+ 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
68
+ probably the skills to scratch that itch of mine: implement `git` in a way that makes tool creation a piece of cake for most.
69
+
70
+ If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
71
+ implementation of 'git' in [Rust](https://www.rust-lang.org).
72
+
73
+ *(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)*
74
+
75
+ ## GitPython
76
+
77
+ GitPython is a python library used to interact with git repositories, high-level like git-porcelain,
78
+ or low-level like git-plumbing.
79
+
80
+ It provides abstractions of git objects for easy access of repository data often backed by calling the `git`
81
+ command-line program.
82
+
83
+ ### DEVELOPMENT STATUS
84
+
85
+ This project is in **maintenance mode**, which means that
86
+
87
+ - …there will be no feature development, unless these are contributed
88
+ - …there will be no bug fixes, unless they are relevant to the safety of users, or contributed
89
+ - …issues will be responded to with waiting times of up to a month
90
+
91
+ The project is open to contributions of all kinds, as well as new maintainers.
92
+
93
+ ### REQUIREMENTS
94
+
95
+ GitPython needs the `git` executable to be installed on the system and available in your
96
+ `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it
97
+ by setting the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
98
+
99
+ - Git (1.7.x or newer)
100
+ - Python >= 3.7
101
+
102
+ The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
103
+ The installer takes care of installing them for you.
104
+
105
+ ### INSTALL
106
+
107
+ 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).
108
+
109
+ #### From PyPI
110
+
111
+ To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run:
112
+
113
+ ```sh
114
+ pip install GitPython
115
+ ```
116
+
117
+ (A distribution package can also be downloaded for manual installation at [the PyPI page](https://pypi.org/project/GitPython/).)
118
+
119
+ #### From downloaded source code
120
+
121
+ If you have downloaded the source code, run this from inside the unpacked `GitPython` directory:
122
+
123
+ ```sh
124
+ pip install .
125
+ ```
126
+
127
+ #### By cloning the source code repository
128
+
129
+ 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:
130
+
131
+ ```sh
132
+ git clone https://github.com/gitpython-developers/GitPython
133
+ cd GitPython
134
+ ./init-tests-after-clone.sh
135
+ ```
136
+
137
+ On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
138
+
139
+ 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`):
140
+
141
+ ```sh
142
+ gh repo clone GitPython
143
+ ```
144
+
145
+ Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html).
146
+
147
+ Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs):
148
+
149
+ ```sh
150
+ pip install -e ".[test]"
151
+ ```
152
+
153
+ In the less common case that you do not want to install test dependencies, `pip install -e .` can be used instead.
154
+
155
+ #### With editable *dependencies* (not preferred, and rarely needed)
156
+
157
+ 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 immediatley 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.
158
+
159
+ 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:
160
+
161
+ ```sh
162
+ pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
163
+ ```
164
+
165
+ 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.
166
+
167
+ 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.
168
+
169
+ ### Limitations
170
+
171
+ #### Leakage of System Resources
172
+
173
+ GitPython is not suited for long-running processes (like daemons) as it tends to
174
+ leak system resources. It was written in a time where destructors (as implemented
175
+ in the `__del__` method) still ran deterministically.
176
+
177
+ In case you still want to use it in such a context, you will want to search the
178
+ codebase for `__del__` implementations and call these yourself when you see fit.
179
+
180
+ Another way assure proper cleanup of resources is to factor out GitPython into a
181
+ separate process which can be dropped periodically.
182
+
183
+ #### Windows support
184
+
185
+ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525).
186
+
187
+ ### RUNNING TESTS
188
+
189
+ _Important_: Right after cloning this repository, please be sure to have executed
190
+ the `./init-tests-after-clone.sh` script in the repository root. Otherwise
191
+ you will encounter test failures.
192
+
193
+ #### Install test dependencies
194
+
195
+ Ensure testing libraries are installed. This is taken care of already if you installed with:
196
+
197
+ ```sh
198
+ pip install -e ".[test]"
199
+ ```
200
+
201
+ If you had installed with a command like `pip install -e .` instead, you can still run
202
+ the above command to add the testing dependencies.
203
+
204
+ #### Test commands
205
+
206
+ To test, run:
207
+
208
+ ```sh
209
+ pytest
210
+ ```
211
+
212
+ To lint, and apply some linting fixes as well as automatic code formatting, run:
213
+
214
+ ```sh
215
+ pre-commit run --all-files
216
+ ```
217
+
218
+ This includes the linting and autoformatting done by Ruff, as well as some other checks.
219
+
220
+ To typecheck, run:
221
+
222
+ ```sh
223
+ mypy
224
+ ```
225
+
226
+ #### CI (and tox)
227
+
228
+ Style and formatting checks, and running tests on all the different supported Python versions, will be performed:
229
+
230
+ - Upon submitting a pull request.
231
+ - On each push, *if* you have a fork with GitHub Actions enabled.
232
+ - Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
233
+
234
+ #### Configuration files
235
+
236
+ Specific tools are all configured in the `./pyproject.toml` file:
237
+
238
+ - `pytest` (test runner)
239
+ - `coverage.py` (code coverage)
240
+ - `ruff` (linter and formatter)
241
+ - `mypy` (type checker)
242
+
243
+ Orchestration tools:
244
+
245
+ - Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file.
246
+ - Configuration for `tox` is in `./tox.ini`.
247
+ - Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`.
248
+
249
+ ### Contributions
250
+
251
+ Please have a look at the [contributions file][contributing].
252
+
253
+ ### INFRASTRUCTURE
254
+
255
+ - [User Documentation](http://gitpython.readthedocs.org)
256
+ - [Questions and Answers](http://stackexchange.com/filters/167317/gitpython)
257
+ - Please post on Stack Overflow and use the `gitpython` tag
258
+ - [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues)
259
+ - Post reproducible bugs and feature requests as a new issue.
260
+ Please be sure to provide the following information if posting bugs:
261
+ - GitPython version (e.g. `import git; git.__version__`)
262
+ - Python version (e.g. `python --version`)
263
+ - The encountered stack-trace, if applicable
264
+ - Enough information to allow reproducing the issue
265
+
266
+ ### How to make a new release
267
+
268
+ 1. Update/verify the **version** in the `VERSION` file.
269
+ 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>`
270
+ 3. Commit everything.
271
+ 4. Run `git tag -s <version>` to tag the version in Git.
272
+ 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`.)
273
+ 6. Run `make release`.
274
+ 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.
275
+
276
+ ### Projects using GitPython
277
+
278
+ - [PyDriller](https://github.com/ishepard/pydriller)
279
+ - [Kivy Designer](https://github.com/kivy/kivy-designer)
280
+ - [Prowl](https://github.com/nettitude/Prowl)
281
+ - [Python Taint](https://github.com/python-security/pyt)
282
+ - [Buster](https://github.com/axitkhurana/buster)
283
+ - [git-ftp](https://github.com/ezyang/git-ftp)
284
+ - [Git-Pandas](https://github.com/wdm0006/git-pandas)
285
+ - [PyGitUp](https://github.com/msiemens/PyGitUp)
286
+ - [PyJFuzz](https://github.com/mseclab/PyJFuzz)
287
+ - [Loki](https://github.com/Neo23x0/Loki)
288
+ - [Omniwallet](https://github.com/OmniLayer/omniwallet)
289
+ - [GitViper](https://github.com/BeayemX/GitViper)
290
+ - [Git Gud](https://github.com/bthayer2365/git-gud)
291
+
292
+ ### LICENSE
293
+
294
+ [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
295
+
296
+ [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
297
+ [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/RECORD ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GitPython-3.1.43.dist-info/AUTHORS,sha256=h1TlPKfp05GA1eKQ15Yl4biR0C0FgivuGSeRA6Q1dz0,2286
2
+ GitPython-3.1.43.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
3
+ GitPython-3.1.43.dist-info/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
4
+ GitPython-3.1.43.dist-info/METADATA,sha256=sAh3r1BMVw5_olGgDmpMS69zBpVr7UEOeRivNHKznfU,13376
5
+ GitPython-3.1.43.dist-info/RECORD,,
6
+ GitPython-3.1.43.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ GitPython-3.1.43.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
+ GitPython-3.1.43.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
9
+ git/__init__.py,sha256=w6fnS0QmwTfEFUSL6rfnpP0lUId2goSguZFOvVX3N3U,8899
10
+ git/__pycache__/__init__.cpython-311.pyc,,
11
+ git/__pycache__/cmd.cpython-311.pyc,,
12
+ git/__pycache__/compat.cpython-311.pyc,,
13
+ git/__pycache__/config.cpython-311.pyc,,
14
+ git/__pycache__/db.cpython-311.pyc,,
15
+ git/__pycache__/diff.cpython-311.pyc,,
16
+ git/__pycache__/exc.cpython-311.pyc,,
17
+ git/__pycache__/remote.cpython-311.pyc,,
18
+ git/__pycache__/types.cpython-311.pyc,,
19
+ git/__pycache__/util.cpython-311.pyc,,
20
+ git/cmd.py,sha256=qd-gIHSk4mfsYjd9YA08cPyO8TMxaibTXAbFnHK71uc,67659
21
+ git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
22
+ git/config.py,sha256=Ald8Xc-G9Shcgx3QCISyXTkL4a6nbc3qll-xUw4YdyY,34924
23
+ git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
24
+ git/diff.py,sha256=IE5aeHL7aP9yxBluYj06IX8nZjoJ_TOM3gG31-Evf_8,27058
25
+ git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
26
+ git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
27
+ git/index/__pycache__/__init__.cpython-311.pyc,,
28
+ git/index/__pycache__/base.cpython-311.pyc,,
29
+ git/index/__pycache__/fun.cpython-311.pyc,,
30
+ git/index/__pycache__/typ.cpython-311.pyc,,
31
+ git/index/__pycache__/util.cpython-311.pyc,,
32
+ git/index/base.py,sha256=A4q4cN_Ifxi8CsAR-7h4KsQ2d3JazBNFZ1ltbAKttgs,60734
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-311.pyc,,
38
+ git/objects/__pycache__/base.cpython-311.pyc,,
39
+ git/objects/__pycache__/blob.cpython-311.pyc,,
40
+ git/objects/__pycache__/commit.cpython-311.pyc,,
41
+ git/objects/__pycache__/fun.cpython-311.pyc,,
42
+ git/objects/__pycache__/tag.cpython-311.pyc,,
43
+ git/objects/__pycache__/tree.cpython-311.pyc,,
44
+ git/objects/__pycache__/util.cpython-311.pyc,,
45
+ git/objects/base.py,sha256=0dqNkSRVH0mk0-7ZKIkGBK7iNYrzLTVxwQFUd6CagsE,10277
46
+ git/objects/blob.py,sha256=zwwq0KfOMYeP5J2tW5CQatoLyeqFRlfkxP1Vwx1h07s,1215
47
+ git/objects/commit.py,sha256=vLZNl1I9zp17Rpge7J66CvsryirEs90jyPTQzoP0JJs,30208
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-311.pyc,,
51
+ git/objects/submodule/__pycache__/base.cpython-311.pyc,,
52
+ git/objects/submodule/__pycache__/root.cpython-311.pyc,,
53
+ git/objects/submodule/__pycache__/util.cpython-311.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=gAx8i-DEwy_Z3R2zLkvetYRV8A56BCcTr3iLuTUTfEM,4467
58
+ git/objects/tree.py,sha256=jJH888SHiP4dGzE-ra1yenQOyya_0C_MkHr06c1gHpM,13849
59
+ git/objects/util.py,sha256=Ml2eqZPKO4y9Hc2vWbXJgpsK3nkN3KGMzbn8AlzLyYQ,23834
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-311.pyc,,
63
+ git/refs/__pycache__/head.cpython-311.pyc,,
64
+ git/refs/__pycache__/log.cpython-311.pyc,,
65
+ git/refs/__pycache__/reference.cpython-311.pyc,,
66
+ git/refs/__pycache__/remote.cpython-311.pyc,,
67
+ git/refs/__pycache__/symbolic.cpython-311.pyc,,
68
+ git/refs/__pycache__/tag.cpython-311.pyc,,
69
+ git/refs/head.py,sha256=GAZpD5EfqSciDXPtgjHY8ZbBixKExJRhojUB-HrrJPg,10491
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=IHQ3BvXgoIN1EvHlyH3vrSaQoDkLOE6nooSC0w183sU,46561
76
+ git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
77
+ git/repo/__pycache__/__init__.cpython-311.pyc,,
78
+ git/repo/__pycache__/base.cpython-311.pyc,,
79
+ git/repo/__pycache__/fun.cpython-311.pyc,,
80
+ git/repo/base.py,sha256=mitfJ8u99CsMpDd7_VRyx-SF8omu2tpf3lqzSaQkKoQ,59353
81
+ git/repo/fun.py,sha256=tEsClpmbOrKMSNIdncOB_6JdikrL1-AfkOFd7xMpD8k,13582
82
+ git/types.py,sha256=xCwpp2Y01lhS0MapHhj04m0P_x34kwSD1Gsou_ZPWj8,10251
83
+ git/util.py,sha256=1E883mnPAFLyFk7ivwnEremsp-uJOTc3ks_QypyLung,43651
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/REQUESTED ADDED
File without changes
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
ILYA/Lib/site-packages/GitPython-3.1.43.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ git
ILYA/Lib/site-packages/_distutils_hack/__init__.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # don't import any costly modules
2
+ import sys
3
+ import os
4
+
5
+
6
+ is_pypy = '__pypy__' in sys.builtin_module_names
7
+
8
+
9
+ def warn_distutils_present():
10
+ if 'distutils' not in sys.modules:
11
+ return
12
+ if is_pypy and sys.version_info < (3, 7):
13
+ # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
14
+ # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
15
+ return
16
+ import warnings
17
+
18
+ warnings.warn(
19
+ "Distutils was imported before Setuptools, but importing Setuptools "
20
+ "also replaces the `distutils` module in `sys.modules`. This may lead "
21
+ "to undesirable behaviors or errors. To avoid these issues, avoid "
22
+ "using distutils directly, ensure that setuptools is installed in the "
23
+ "traditional way (e.g. not an editable install), and/or make sure "
24
+ "that setuptools is always imported before distutils."
25
+ )
26
+
27
+
28
+ def clear_distutils():
29
+ if 'distutils' not in sys.modules:
30
+ return
31
+ import warnings
32
+
33
+ warnings.warn("Setuptools is replacing distutils.")
34
+ mods = [
35
+ name
36
+ for name in sys.modules
37
+ if name == "distutils" or name.startswith("distutils.")
38
+ ]
39
+ for name in mods:
40
+ del sys.modules[name]
41
+
42
+
43
+ def enabled():
44
+ """
45
+ Allow selection of distutils by environment variable.
46
+ """
47
+ which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
48
+ return which == 'local'
49
+
50
+
51
+ def ensure_local_distutils():
52
+ import importlib
53
+
54
+ clear_distutils()
55
+
56
+ # With the DistutilsMetaFinder in place,
57
+ # perform an import to cause distutils to be
58
+ # loaded from setuptools._distutils. Ref #2906.
59
+ with shim():
60
+ importlib.import_module('distutils')
61
+
62
+ # check that submodules load as expected
63
+ core = importlib.import_module('distutils.core')
64
+ assert '_distutils' in core.__file__, core.__file__
65
+ assert 'setuptools._distutils.log' not in sys.modules
66
+
67
+
68
+ def do_override():
69
+ """
70
+ Ensure that the local copy of distutils is preferred over stdlib.
71
+
72
+ See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
73
+ for more motivation.
74
+ """
75
+ if enabled():
76
+ warn_distutils_present()
77
+ ensure_local_distutils()
78
+
79
+
80
+ class _TrivialRe:
81
+ def __init__(self, *patterns):
82
+ self._patterns = patterns
83
+
84
+ def match(self, string):
85
+ return all(pat in string for pat in self._patterns)
86
+
87
+
88
+ class DistutilsMetaFinder:
89
+ def find_spec(self, fullname, path, target=None):
90
+ # optimization: only consider top level modules and those
91
+ # found in the CPython test suite.
92
+ if path is not None and not fullname.startswith('test.'):
93
+ return
94
+
95
+ method_name = 'spec_for_{fullname}'.format(**locals())
96
+ method = getattr(self, method_name, lambda: None)
97
+ return method()
98
+
99
+ def spec_for_distutils(self):
100
+ if self.is_cpython():
101
+ return
102
+
103
+ import importlib
104
+ import importlib.abc
105
+ import importlib.util
106
+
107
+ try:
108
+ mod = importlib.import_module('setuptools._distutils')
109
+ except Exception:
110
+ # There are a couple of cases where setuptools._distutils
111
+ # may not be present:
112
+ # - An older Setuptools without a local distutils is
113
+ # taking precedence. Ref #2957.
114
+ # - Path manipulation during sitecustomize removes
115
+ # setuptools from the path but only after the hook
116
+ # has been loaded. Ref #2980.
117
+ # In either case, fall back to stdlib behavior.
118
+ return
119
+
120
+ class DistutilsLoader(importlib.abc.Loader):
121
+ def create_module(self, spec):
122
+ mod.__name__ = 'distutils'
123
+ return mod
124
+
125
+ def exec_module(self, module):
126
+ pass
127
+
128
+ return importlib.util.spec_from_loader(
129
+ 'distutils', DistutilsLoader(), origin=mod.__file__
130
+ )
131
+
132
+ @staticmethod
133
+ def is_cpython():
134
+ """
135
+ Suppress supplying distutils for CPython (build and tests).
136
+ Ref #2965 and #3007.
137
+ """
138
+ return os.path.isfile('pybuilddir.txt')
139
+
140
+ def spec_for_pip(self):
141
+ """
142
+ Ensure stdlib distutils when running under pip.
143
+ See pypa/pip#8761 for rationale.
144
+ """
145
+ if self.pip_imported_during_build():
146
+ return
147
+ clear_distutils()
148
+ self.spec_for_distutils = lambda: None
149
+
150
+ @classmethod
151
+ def pip_imported_during_build(cls):
152
+ """
153
+ Detect if pip is being imported in a build script. Ref #2355.
154
+ """
155
+ import traceback
156
+
157
+ return any(
158
+ cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
159
+ )
160
+
161
+ @staticmethod
162
+ def frame_file_is_setup(frame):
163
+ """
164
+ Return True if the indicated frame suggests a setup.py file.
165
+ """
166
+ # some frames may not have __file__ (#2940)
167
+ return frame.f_globals.get('__file__', '').endswith('setup.py')
168
+
169
+ def spec_for_sensitive_tests(self):
170
+ """
171
+ Ensure stdlib distutils when running select tests under CPython.
172
+
173
+ python/cpython#91169
174
+ """
175
+ clear_distutils()
176
+ self.spec_for_distutils = lambda: None
177
+
178
+ sensitive_tests = (
179
+ [
180
+ 'test.test_distutils',
181
+ 'test.test_peg_generator',
182
+ 'test.test_importlib',
183
+ ]
184
+ if sys.version_info < (3, 10)
185
+ else [
186
+ 'test.test_distutils',
187
+ ]
188
+ )
189
+
190
+
191
+ for name in DistutilsMetaFinder.sensitive_tests:
192
+ setattr(
193
+ DistutilsMetaFinder,
194
+ f'spec_for_{name}',
195
+ DistutilsMetaFinder.spec_for_sensitive_tests,
196
+ )
197
+
198
+
199
+ DISTUTILS_FINDER = DistutilsMetaFinder()
200
+
201
+
202
+ def add_shim():
203
+ DISTUTILS_FINDER in sys.meta_path or insert_shim()
204
+
205
+
206
+ class shim:
207
+ def __enter__(self):
208
+ insert_shim()
209
+
210
+ def __exit__(self, exc, value, tb):
211
+ remove_shim()
212
+
213
+
214
+ def insert_shim():
215
+ sys.meta_path.insert(0, DISTUTILS_FINDER)
216
+
217
+
218
+ def remove_shim():
219
+ try:
220
+ sys.meta_path.remove(DISTUTILS_FINDER)
221
+ except ValueError:
222
+ pass
ILYA/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (11.2 kB). View file
 
ILYA/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc ADDED
Binary file (361 Bytes). View file
 
ILYA/Lib/site-packages/_distutils_hack/override.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __import__('_distutils_hack').do_override()
ILYA/Lib/site-packages/distutils-precedence.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2638ce9e2500e572a5e0de7faed6661eb569d1b696fcba07b0dd223da5f5d224
3
+ size 151
ILYA/Lib/site-packages/git/__init__.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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.43'
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
ILYA/Lib/site-packages/git/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (7.3 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/cmd.cpython-311.pyc ADDED
Binary file (68.9 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/compat.cpython-311.pyc ADDED
Binary file (5.18 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/config.cpython-311.pyc ADDED
Binary file (45.9 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/db.cpython-311.pyc ADDED
Binary file (3.76 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/diff.cpython-311.pyc ADDED
Binary file (28.3 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/exc.cpython-311.pyc ADDED
Binary file (11.8 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/remote.cpython-311.pyc ADDED
Binary file (52.9 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/types.cpython-311.pyc ADDED
Binary file (6.61 kB). View file
 
ILYA/Lib/site-packages/git/__pycache__/util.cpython-311.pyc ADDED
Binary file (60.4 kB). View file
 
ILYA/Lib/site-packages/git/cmd.py ADDED
@@ -0,0 +1,1723 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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
+ **subprocess_kwargs,
1273
+ )
1274
+ except cmd_not_found_exception as err:
1275
+ raise GitCommandNotFound(redacted_command, err) from err
1276
+ else:
1277
+ # Replace with a typeguard for Popen[bytes]?
1278
+ proc.stdout = cast(BinaryIO, proc.stdout)
1279
+ proc.stderr = cast(BinaryIO, proc.stderr)
1280
+
1281
+ if as_process:
1282
+ return self.AutoInterrupt(proc, command)
1283
+
1284
+ if sys.platform != "win32" and kill_after_timeout is not None:
1285
+ # Help mypy figure out this is not None even when used inside communicate().
1286
+ timeout = kill_after_timeout
1287
+
1288
+ def kill_process(pid: int) -> None:
1289
+ """Callback to kill a process.
1290
+
1291
+ This callback implementation would be ineffective and unsafe on Windows.
1292
+ """
1293
+ p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
1294
+ child_pids = []
1295
+ if p.stdout is not None:
1296
+ for line in p.stdout:
1297
+ if len(line.split()) > 0:
1298
+ local_pid = (line.split())[0]
1299
+ if local_pid.isdigit():
1300
+ child_pids.append(int(local_pid))
1301
+ try:
1302
+ os.kill(pid, signal.SIGKILL)
1303
+ for child_pid in child_pids:
1304
+ try:
1305
+ os.kill(child_pid, signal.SIGKILL)
1306
+ except OSError:
1307
+ pass
1308
+ # Tell the main routine that the process was killed.
1309
+ kill_check.set()
1310
+ except OSError:
1311
+ # It is possible that the process gets completed in the duration
1312
+ # after timeout happens and before we try to kill the process.
1313
+ pass
1314
+ return
1315
+
1316
+ def communicate() -> Tuple[AnyStr, AnyStr]:
1317
+ watchdog.start()
1318
+ out, err = proc.communicate()
1319
+ watchdog.cancel()
1320
+ if kill_check.is_set():
1321
+ err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
1322
+ " ".join(redacted_command),
1323
+ timeout,
1324
+ )
1325
+ if not universal_newlines:
1326
+ err = err.encode(defenc)
1327
+ return out, err
1328
+
1329
+ # END helpers
1330
+
1331
+ kill_check = threading.Event()
1332
+ watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
1333
+ else:
1334
+ communicate = proc.communicate
1335
+
1336
+ # Wait for the process to return.
1337
+ status = 0
1338
+ stdout_value: Union[str, bytes] = b""
1339
+ stderr_value: Union[str, bytes] = b""
1340
+ newline = "\n" if universal_newlines else b"\n"
1341
+ try:
1342
+ if output_stream is None:
1343
+ stdout_value, stderr_value = communicate()
1344
+ # Strip trailing "\n".
1345
+ if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
1346
+ stdout_value = stdout_value[:-1]
1347
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1348
+ stderr_value = stderr_value[:-1]
1349
+
1350
+ status = proc.returncode
1351
+ else:
1352
+ max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
1353
+ stream_copy(proc.stdout, output_stream, max_chunk_size)
1354
+ stdout_value = proc.stdout.read()
1355
+ stderr_value = proc.stderr.read()
1356
+ # Strip trailing "\n".
1357
+ if stderr_value.endswith(newline): # type: ignore[arg-type]
1358
+ stderr_value = stderr_value[:-1]
1359
+ status = proc.wait()
1360
+ # END stdout handling
1361
+ finally:
1362
+ proc.stdout.close()
1363
+ proc.stderr.close()
1364
+
1365
+ if self.GIT_PYTHON_TRACE == "full":
1366
+ cmdstr = " ".join(redacted_command)
1367
+
1368
+ def as_text(stdout_value: Union[bytes, str]) -> str:
1369
+ return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
1370
+
1371
+ # END as_text
1372
+
1373
+ if stderr_value:
1374
+ _logger.info(
1375
+ "%s -> %d; stdout: '%s'; stderr: '%s'",
1376
+ cmdstr,
1377
+ status,
1378
+ as_text(stdout_value),
1379
+ safe_decode(stderr_value),
1380
+ )
1381
+ elif stdout_value:
1382
+ _logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
1383
+ else:
1384
+ _logger.info("%s -> %d", cmdstr, status)
1385
+ # END handle debug printing
1386
+
1387
+ if with_exceptions and status != 0:
1388
+ raise GitCommandError(redacted_command, status, stderr_value, stdout_value)
1389
+
1390
+ if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream.
1391
+ stdout_value = safe_decode(stdout_value)
1392
+
1393
+ # Allow access to the command's status code.
1394
+ if with_extended_output:
1395
+ return (status, stdout_value, safe_decode(stderr_value))
1396
+ else:
1397
+ return stdout_value
1398
+
1399
+ def environment(self) -> Dict[str, str]:
1400
+ return self._environment
1401
+
1402
+ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
1403
+ """Set environment variables for future git invocations. Return all changed
1404
+ values in a format that can be passed back into this function to revert the
1405
+ changes.
1406
+
1407
+ Examples::
1408
+
1409
+ old_env = self.update_environment(PWD='/tmp')
1410
+ self.update_environment(**old_env)
1411
+
1412
+ :param kwargs:
1413
+ Environment variables to use for git processes.
1414
+
1415
+ :return:
1416
+ Dict that maps environment variables to their old values
1417
+ """
1418
+ old_env = {}
1419
+ for key, value in kwargs.items():
1420
+ # Set value if it is None.
1421
+ if value is not None:
1422
+ old_env[key] = self._environment.get(key)
1423
+ self._environment[key] = value
1424
+ # Remove key from environment if its value is None.
1425
+ elif key in self._environment:
1426
+ old_env[key] = self._environment[key]
1427
+ del self._environment[key]
1428
+ return old_env
1429
+
1430
+ @contextlib.contextmanager
1431
+ def custom_environment(self, **kwargs: Any) -> Iterator[None]:
1432
+ """A context manager around the above :meth:`update_environment` method to
1433
+ restore the environment back to its previous state after operation.
1434
+
1435
+ Examples::
1436
+
1437
+ with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
1438
+ repo.remotes.origin.fetch()
1439
+
1440
+ :param kwargs:
1441
+ See :meth:`update_environment`.
1442
+ """
1443
+ old_env = self.update_environment(**kwargs)
1444
+ try:
1445
+ yield
1446
+ finally:
1447
+ self.update_environment(**old_env)
1448
+
1449
+ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]:
1450
+ if len(name) == 1:
1451
+ if value is True:
1452
+ return ["-%s" % name]
1453
+ elif value not in (False, None):
1454
+ if split_single_char_options:
1455
+ return ["-%s" % name, "%s" % value]
1456
+ else:
1457
+ return ["-%s%s" % (name, value)]
1458
+ else:
1459
+ if value is True:
1460
+ return ["--%s" % dashify(name)]
1461
+ elif value is not False and value is not None:
1462
+ return ["--%s=%s" % (dashify(name), value)]
1463
+ return []
1464
+
1465
+ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
1466
+ """Transform Python-style kwargs into git command line options."""
1467
+ args = []
1468
+ for k, v in kwargs.items():
1469
+ if isinstance(v, (list, tuple)):
1470
+ for value in v:
1471
+ args += self.transform_kwarg(k, value, split_single_char_options)
1472
+ else:
1473
+ args += self.transform_kwarg(k, v, split_single_char_options)
1474
+ return args
1475
+
1476
+ @classmethod
1477
+ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
1478
+ outlist = []
1479
+ if isinstance(arg_list, (list, tuple)):
1480
+ for arg in arg_list:
1481
+ outlist.extend(cls._unpack_args(arg))
1482
+ else:
1483
+ outlist.append(str(arg_list))
1484
+
1485
+ return outlist
1486
+
1487
+ def __call__(self, **kwargs: Any) -> "Git":
1488
+ """Specify command line options to the git executable for a subcommand call.
1489
+
1490
+ :param kwargs:
1491
+ A dict of keyword arguments.
1492
+ These arguments are passed as in :meth:`_call_process`, but will be passed
1493
+ to the git command rather than the subcommand.
1494
+
1495
+ Examples::
1496
+
1497
+ git(work_tree='/tmp').difftool()
1498
+ """
1499
+ self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
1500
+ return self
1501
+
1502
+ @overload
1503
+ def _call_process(
1504
+ self, method: str, *args: None, **kwargs: None
1505
+ ) -> str: ... # If no args were given, execute the call with all defaults.
1506
+
1507
+ @overload
1508
+ def _call_process(
1509
+ self,
1510
+ method: str,
1511
+ istream: int,
1512
+ as_process: Literal[True],
1513
+ *args: Any,
1514
+ **kwargs: Any,
1515
+ ) -> "Git.AutoInterrupt": ...
1516
+
1517
+ @overload
1518
+ def _call_process(
1519
+ self, method: str, *args: Any, **kwargs: Any
1520
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ...
1521
+
1522
+ def _call_process(
1523
+ self, method: str, *args: Any, **kwargs: Any
1524
+ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
1525
+ """Run the given git command with the specified arguments and return the result
1526
+ as a string.
1527
+
1528
+ :param method:
1529
+ The command. Contained ``_`` characters will be converted to hyphens, such
1530
+ as in ``ls_files`` to call ``ls-files``.
1531
+
1532
+ :param args:
1533
+ The list of arguments. If ``None`` is included, it will be pruned.
1534
+ This allows your commands to call git more conveniently, as ``None`` is
1535
+ realized as non-existent.
1536
+
1537
+ :param kwargs:
1538
+ Contains key-values for the following:
1539
+
1540
+ - The :meth:`execute()` kwds, as listed in ``execute_kwargs``.
1541
+ - "Command options" to be converted by :meth:`transform_kwargs`.
1542
+ - The ``insert_kwargs_after`` key which its value must match one of
1543
+ ``*args``.
1544
+
1545
+ It also contains any command options, to be appended after the matched arg.
1546
+
1547
+ Examples::
1548
+
1549
+ git.rev_list('master', max_count=10, header=True)
1550
+
1551
+ turns into::
1552
+
1553
+ git rev-list max-count 10 --header master
1554
+
1555
+ :return:
1556
+ Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
1557
+ default (especially ``as_process = False``, ``stdout_as_string = True``) and
1558
+ return :class:`str`.
1559
+ """
1560
+ # Handle optional arguments prior to calling transform_kwargs.
1561
+ # Otherwise these'll end up in args, which is bad.
1562
+ exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
1563
+ opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
1564
+
1565
+ insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None)
1566
+
1567
+ # Prepare the argument list.
1568
+
1569
+ opt_args = self.transform_kwargs(**opts_kwargs)
1570
+ ext_args = self._unpack_args([a for a in args if a is not None])
1571
+
1572
+ if insert_after_this_arg is None:
1573
+ args_list = opt_args + ext_args
1574
+ else:
1575
+ try:
1576
+ index = ext_args.index(insert_after_this_arg)
1577
+ except ValueError as err:
1578
+ raise ValueError(
1579
+ "Couldn't find argument '%s' in args %s to insert cmd options after"
1580
+ % (insert_after_this_arg, str(ext_args))
1581
+ ) from err
1582
+ # END handle error
1583
+ args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :]
1584
+ # END handle opts_kwargs
1585
+
1586
+ call = [self.GIT_PYTHON_GIT_EXECUTABLE]
1587
+
1588
+ # Add persistent git options.
1589
+ call.extend(self._persistent_git_options)
1590
+
1591
+ # Add the git options, then reset to empty to avoid side effects.
1592
+ call.extend(self._git_options)
1593
+ self._git_options = ()
1594
+
1595
+ call.append(dashify(method))
1596
+ call.extend(args_list)
1597
+
1598
+ return self.execute(call, **exec_kwargs)
1599
+
1600
+ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
1601
+ """
1602
+ :param header_line:
1603
+ A line of the form::
1604
+
1605
+ <hex_sha> type_string size_as_int
1606
+
1607
+ :return:
1608
+ (hex_sha, type_string, size_as_int)
1609
+
1610
+ :raise ValueError:
1611
+ If the header contains indication for an error due to incorrect input sha.
1612
+ """
1613
+ tokens = header_line.split()
1614
+ if len(tokens) != 3:
1615
+ if not tokens:
1616
+ err_msg = (
1617
+ f"SHA is empty, possible dubious ownership in the repository "
1618
+ f"""at {self._working_dir}.\n If this is unintended run:\n\n """
1619
+ f""" "git config --global --add safe.directory {self._working_dir}" """
1620
+ )
1621
+ raise ValueError(err_msg)
1622
+ else:
1623
+ raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip()))
1624
+ # END handle actual return value
1625
+ # END error handling
1626
+
1627
+ if len(tokens[0]) != 40:
1628
+ raise ValueError("Failed to parse header: %r" % header_line)
1629
+ return (tokens[0], tokens[1], int(tokens[2]))
1630
+
1631
+ def _prepare_ref(self, ref: AnyStr) -> bytes:
1632
+ # Required for command to separate refs on stdin, as bytes.
1633
+ if isinstance(ref, bytes):
1634
+ # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text.
1635
+ refstr: str = ref.decode("ascii")
1636
+ elif not isinstance(ref, str):
1637
+ refstr = str(ref) # Could be ref-object.
1638
+ else:
1639
+ refstr = ref
1640
+
1641
+ if not refstr.endswith("\n"):
1642
+ refstr += "\n"
1643
+ return refstr.encode(defenc)
1644
+
1645
+ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt":
1646
+ cur_val = getattr(self, attr_name)
1647
+ if cur_val is not None:
1648
+ return cur_val
1649
+
1650
+ options = {"istream": PIPE, "as_process": True}
1651
+ options.update(kwargs)
1652
+
1653
+ cmd = self._call_process(cmd_name, *args, **options)
1654
+ setattr(self, attr_name, cmd)
1655
+ cmd = cast("Git.AutoInterrupt", cmd)
1656
+ return cmd
1657
+
1658
+ def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]:
1659
+ if cmd.stdin and cmd.stdout:
1660
+ cmd.stdin.write(self._prepare_ref(ref))
1661
+ cmd.stdin.flush()
1662
+ return self._parse_object_header(cmd.stdout.readline())
1663
+ else:
1664
+ raise ValueError("cmd stdin was empty")
1665
+
1666
+ def get_object_header(self, ref: str) -> Tuple[str, str, int]:
1667
+ """Use this method to quickly examine the type and size of the object behind the
1668
+ given ref.
1669
+
1670
+ :note:
1671
+ The method will only suffer from the costs of command invocation once and
1672
+ reuses the command in subsequent calls.
1673
+
1674
+ :return:
1675
+ (hexsha, type_string, size_as_int)
1676
+ """
1677
+ cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
1678
+ return self.__get_object_header(cmd, ref)
1679
+
1680
+ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
1681
+ """Similar to :meth:`get_object_header`, but returns object data as well.
1682
+
1683
+ :return:
1684
+ (hexsha, type_string, size_as_int, data_string)
1685
+
1686
+ :note:
1687
+ Not threadsafe.
1688
+ """
1689
+ hexsha, typename, size, stream = self.stream_object_data(ref)
1690
+ data = stream.read(size)
1691
+ del stream
1692
+ return (hexsha, typename, size, data)
1693
+
1694
+ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
1695
+ """Similar to :meth:`get_object_data`, but returns the data as a stream.
1696
+
1697
+ :return:
1698
+ (hexsha, type_string, size_as_int, stream)
1699
+
1700
+ :note:
1701
+ This method is not threadsafe. You need one independent :class:`Git`
1702
+ instance per thread to be safe!
1703
+ """
1704
+ cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
1705
+ hexsha, typename, size = self.__get_object_header(cmd, ref)
1706
+ cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()
1707
+ return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout))
1708
+
1709
+ def clear_cache(self) -> "Git":
1710
+ """Clear all kinds of internal caches to release resources.
1711
+
1712
+ Currently persistent commands will be interrupted.
1713
+
1714
+ :return:
1715
+ self
1716
+ """
1717
+ for cmd in (self.cat_file_all, self.cat_file_header):
1718
+ if cmd:
1719
+ cmd.__del__()
1720
+
1721
+ self.cat_file_all = None
1722
+ self.cat_file_header = None
1723
+ return self
ILYA/Lib/site-packages/git/compat.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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
ILYA/Lib/site-packages/git/config.py ADDED
@@ -0,0 +1,944 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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[-1] == "\\":
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
ILYA/Lib/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
ILYA/Lib/site-packages/git/diff.py ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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":
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:
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:
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) -> 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()
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":
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
ILYA/Lib/site-packages/git/exc.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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)
ILYA/Lib/site-packages/git/index/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Initialize the index package."""
5
+
6
+ __all__ = [
7
+ "BaseIndexEntry",
8
+ "BlobFilter",
9
+ "CheckoutError",
10
+ "IndexEntry",
11
+ "IndexFile",
12
+ "StageType",
13
+ ]
14
+
15
+ from .base import CheckoutError, IndexFile
16
+ from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType
ILYA/Lib/site-packages/git/index/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (573 Bytes). View file
 
ILYA/Lib/site-packages/git/index/__pycache__/base.cpython-311.pyc ADDED
Binary file (69.5 kB). View file
 
ILYA/Lib/site-packages/git/index/__pycache__/fun.cpython-311.pyc ADDED
Binary file (18.9 kB). View file
 
ILYA/Lib/site-packages/git/index/__pycache__/typ.cpython-311.pyc ADDED
Binary file (10.3 kB). View file
 
ILYA/Lib/site-packages/git/index/__pycache__/util.cpython-311.pyc ADDED
Binary file (6.24 kB). View file
 
ILYA/Lib/site-packages/git/index/base.py ADDED
@@ -0,0 +1,1518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) and contributors
2
+ #
3
+ # This module is part of GitPython and is released under the
4
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5
+
6
+ """Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
7
+ of index manipulations such as querying and merging."""
8
+
9
+ __all__ = ["IndexFile", "CheckoutError", "StageType"]
10
+
11
+ import contextlib
12
+ import datetime
13
+ import glob
14
+ from io import BytesIO
15
+ import os
16
+ import os.path as osp
17
+ from stat import S_ISLNK
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+
22
+ from gitdb.base import IStream
23
+ from gitdb.db import MemoryDB
24
+
25
+ from git.compat import defenc, force_bytes
26
+ import git.diff as git_diff
27
+ from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
28
+ from git.objects import Blob, Commit, Object, Submodule, Tree
29
+ from git.objects.util import Serializable
30
+ from git.util import (
31
+ LazyMixin,
32
+ LockedFD,
33
+ join_path_native,
34
+ file_contents_ro,
35
+ to_native_path_linux,
36
+ unbare_repo,
37
+ to_bin_sha,
38
+ )
39
+
40
+ from .fun import (
41
+ S_IFGITLINK,
42
+ aggressive_tree_merge,
43
+ entry_key,
44
+ read_cache,
45
+ run_commit_hook,
46
+ stat_mode_to_index_mode,
47
+ write_cache,
48
+ write_tree_from_cache,
49
+ )
50
+ from .typ import BaseIndexEntry, IndexEntry, StageType
51
+ from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
52
+
53
+ # typing -----------------------------------------------------------------------------
54
+
55
+ from typing import (
56
+ Any,
57
+ BinaryIO,
58
+ Callable,
59
+ Dict,
60
+ Generator,
61
+ IO,
62
+ Iterable,
63
+ Iterator,
64
+ List,
65
+ NoReturn,
66
+ Sequence,
67
+ TYPE_CHECKING,
68
+ Tuple,
69
+ Union,
70
+ )
71
+
72
+ from git.types import Literal, PathLike
73
+
74
+ if TYPE_CHECKING:
75
+ from subprocess import Popen
76
+
77
+ from git.refs.reference import Reference
78
+ from git.repo import Repo
79
+ from git.util import Actor
80
+
81
+
82
+ Treeish = Union[Tree, Commit, str, bytes]
83
+
84
+ # ------------------------------------------------------------------------------------
85
+
86
+
87
+ @contextlib.contextmanager
88
+ def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
89
+ """Create a named temporary file git subprocesses can open, deleting it afterward.
90
+
91
+ :param directory:
92
+ The directory in which the file is created.
93
+
94
+ :return:
95
+ A context manager object that creates the file and provides its name on entry,
96
+ and deletes it on exit.
97
+ """
98
+ if sys.platform == "win32":
99
+ fd, name = tempfile.mkstemp(dir=directory)
100
+ os.close(fd)
101
+ try:
102
+ yield name
103
+ finally:
104
+ os.remove(name)
105
+ else:
106
+ with tempfile.NamedTemporaryFile(dir=directory) as ctx:
107
+ yield ctx.name
108
+
109
+
110
+ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
111
+ """An Index that can be manipulated using a native implementation in order to save
112
+ git command function calls wherever possible.
113
+
114
+ This provides custom merging facilities allowing to merge without actually changing
115
+ your index or your working tree. This way you can perform your own test merges based
116
+ on the index only without having to deal with the working copy. This is useful in
117
+ case of partial working trees.
118
+
119
+ Entries:
120
+
121
+ The index contains an entries dict whose keys are tuples of type
122
+ :class:`~git.index.typ.IndexEntry` to facilitate access.
123
+
124
+ You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
125
+
126
+ index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
127
+
128
+ Make sure you use :meth:`index.write() <write>` once you are done manipulating the
129
+ index directly before operating on it using the git command.
130
+ """
131
+
132
+ __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
133
+
134
+ _VERSION = 2
135
+ """The latest version we support."""
136
+
137
+ S_IFGITLINK = S_IFGITLINK
138
+ """Flags for a submodule."""
139
+
140
+ def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
141
+ """Initialize this Index instance, optionally from the given `file_path`.
142
+
143
+ If no `file_path` is given, we will be created from the current index file.
144
+
145
+ If a stream is not given, the stream will be initialized from the current
146
+ repository's index on demand.
147
+ """
148
+ self.repo = repo
149
+ self.version = self._VERSION
150
+ self._extension_data = b""
151
+ self._file_path: PathLike = file_path or self._index_path()
152
+
153
+ def _set_cache_(self, attr: str) -> None:
154
+ if attr == "entries":
155
+ try:
156
+ fd = os.open(self._file_path, os.O_RDONLY)
157
+ except OSError:
158
+ # In new repositories, there may be no index, which means we are empty.
159
+ self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
160
+ return
161
+ # END exception handling
162
+
163
+ try:
164
+ stream = file_contents_ro(fd, stream=True, allow_mmap=True)
165
+ finally:
166
+ os.close(fd)
167
+
168
+ self._deserialize(stream)
169
+ else:
170
+ super()._set_cache_(attr)
171
+
172
+ def _index_path(self) -> PathLike:
173
+ if self.repo.git_dir:
174
+ return join_path_native(self.repo.git_dir, "index")
175
+ else:
176
+ raise GitCommandError("No git directory given to join index path")
177
+
178
+ @property
179
+ def path(self) -> PathLike:
180
+ """:return: Path to the index file we are representing"""
181
+ return self._file_path
182
+
183
+ def _delete_entries_cache(self) -> None:
184
+ """Safely clear the entries cache so it can be recreated."""
185
+ try:
186
+ del self.entries
187
+ except AttributeError:
188
+ # It failed in Python 2.6.5 with AttributeError.
189
+ # FIXME: Look into whether we can just remove this except clause now.
190
+ pass
191
+ # END exception handling
192
+
193
+ # { Serializable Interface
194
+
195
+ def _deserialize(self, stream: IO) -> "IndexFile":
196
+ """Initialize this instance with index values read from the given stream."""
197
+ self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
198
+ return self
199
+
200
+ def _entries_sorted(self) -> List[IndexEntry]:
201
+ """:return: List of entries, in a sorted fashion, first by path, then by stage"""
202
+ return sorted(self.entries.values(), key=lambda e: (e.path, e.stage))
203
+
204
+ def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile":
205
+ entries = self._entries_sorted()
206
+ extension_data = self._extension_data # type: Union[None, bytes]
207
+ if ignore_extension_data:
208
+ extension_data = None
209
+ write_cache(entries, stream, extension_data)
210
+ return self
211
+
212
+ # } END serializable interface
213
+
214
+ def write(
215
+ self,
216
+ file_path: Union[None, PathLike] = None,
217
+ ignore_extension_data: bool = False,
218
+ ) -> None:
219
+ """Write the current state to our file path or to the given one.
220
+
221
+ :param file_path:
222
+ If ``None``, we will write to our stored file path from which we have been
223
+ initialized. Otherwise we write to the given file path. Please note that
224
+ this will change the `file_path` of this index to the one you gave.
225
+
226
+ :param ignore_extension_data:
227
+ If ``True``, the TREE type extension data read in the index will not be
228
+ written to disk. NOTE that no extension data is actually written. Use this
229
+ if you have altered the index and would like to use
230
+ :manpage:`git-write-tree(1)` afterwards to create a tree representing your
231
+ written changes. If this data is present in the written index,
232
+ :manpage:`git-write-tree(1)` will instead write the stored/cached tree.
233
+ Alternatively, use :meth:`write_tree` to handle this case automatically.
234
+ """
235
+ # Make sure we have our entries read before getting a write lock.
236
+ # Otherwise it would be done when streaming.
237
+ # This can happen if one doesn't change the index, but writes it right away.
238
+ self.entries # noqa: B018
239
+ lfd = LockedFD(file_path or self._file_path)
240
+ stream = lfd.open(write=True, stream=True)
241
+
242
+ try:
243
+ self._serialize(stream, ignore_extension_data)
244
+ except BaseException:
245
+ lfd.rollback()
246
+ raise
247
+
248
+ lfd.commit()
249
+
250
+ # Make sure we represent what we have written.
251
+ if file_path is not None:
252
+ self._file_path = file_path
253
+
254
+ @post_clear_cache
255
+ @default_index
256
+ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
257
+ """Merge the given `rhs` treeish into the current index, possibly taking
258
+ a common base treeish into account.
259
+
260
+ As opposed to the :func:`from_tree` method, this allows you to use an already
261
+ existing tree as the left side of the merge.
262
+
263
+ :param rhs:
264
+ Treeish reference pointing to the 'other' side of the merge.
265
+
266
+ :param base:
267
+ Optional treeish reference pointing to the common base of `rhs` and this
268
+ index which equals lhs.
269
+
270
+ :return:
271
+ self (containing the merge and possibly unmerged entries in case of
272
+ conflicts)
273
+
274
+ :raise git.exc.GitCommandError:
275
+ If there is a merge conflict. The error will be raised at the first
276
+ conflicting path. If you want to have proper merge resolution to be done by
277
+ yourself, you have to commit the changed index (or make a valid tree from
278
+ it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
279
+ """
280
+ # -i : ignore working tree status
281
+ # --aggressive : handle more merge cases
282
+ # -m : do an actual merge
283
+ args: List[Union[Treeish, str]] = ["--aggressive", "-i", "-m"]
284
+ if base is not None:
285
+ args.append(base)
286
+ args.append(rhs)
287
+
288
+ self.repo.git.read_tree(args)
289
+ return self
290
+
291
+ @classmethod
292
+ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
293
+ """Merge the given treeish revisions into a new index which is returned.
294
+
295
+ This method behaves like ``git-read-tree --aggressive`` when doing the merge.
296
+
297
+ :param repo:
298
+ The repository treeish are located in.
299
+
300
+ :param tree_sha:
301
+ 20 byte or 40 byte tree sha or tree objects.
302
+
303
+ :return:
304
+ New :class:`IndexFile` instance. Its path will be undefined.
305
+ If you intend to write such a merged Index, supply an alternate
306
+ ``file_path`` to its :meth:`write` method.
307
+ """
308
+ tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
309
+ base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
310
+
311
+ inst = cls(repo)
312
+ # Convert to entries dict.
313
+ entries: Dict[Tuple[PathLike, int], IndexEntry] = dict(
314
+ zip(
315
+ ((e.path, e.stage) for e in base_entries),
316
+ (IndexEntry.from_base(e) for e in base_entries),
317
+ )
318
+ )
319
+
320
+ inst.entries = entries
321
+ return inst
322
+
323
+ @classmethod
324
+ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
325
+ R"""Merge the given treeish revisions into a new index which is returned.
326
+ The original index will remain unaltered.
327
+
328
+ :param repo:
329
+ The repository treeish are located in.
330
+
331
+ :param treeish:
332
+ One, two or three :class:`~git.objects.tree.Tree` objects,
333
+ :class:`~git.objects.commit.Commit`\s or 40 byte hexshas.
334
+
335
+ The result changes according to the amount of trees:
336
+
337
+ 1. If 1 Tree is given, it will just be read into a new index.
338
+ 2. If 2 Trees are given, they will be merged into a new index using a two
339
+ way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
340
+ one. It behaves like a fast-forward.
341
+ 3. If 3 Trees are given, a 3-way merge will be performed with the first tree
342
+ being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current'
343
+ tree, tree 3 is the 'other' one.
344
+
345
+ :param kwargs:
346
+ Additional arguments passed to :manpage:`git-read-tree(1)`.
347
+
348
+ :return:
349
+ New :class:`IndexFile` instance. It will point to a temporary index location
350
+ which does not exist anymore. If you intend to write such a merged Index,
351
+ supply an alternate ``file_path`` to its :meth:`write` method.
352
+
353
+ :note:
354
+ In the three-way merge case, ``--aggressive`` will be specified to
355
+ automatically resolve more cases in a commonly correct manner. Specify
356
+ ``trivial=True`` as a keyword argument to override that.
357
+
358
+ As the underlying :manpage:`git-read-tree(1)` command takes into account the
359
+ current index, it will be temporarily moved out of the way to prevent any
360
+ unexpected interference.
361
+ """
362
+ if len(treeish) == 0 or len(treeish) > 3:
363
+ raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
364
+
365
+ arg_list: List[Union[Treeish, str]] = []
366
+ # Ignore that the working tree and index possibly are out of date.
367
+ if len(treeish) > 1:
368
+ # Drop unmerged entries when reading our index and merging.
369
+ arg_list.append("--reset")
370
+ # Handle non-trivial cases the way a real merge does.
371
+ arg_list.append("--aggressive")
372
+ # END merge handling
373
+
374
+ # Create the temporary file in the .git directory to be sure renaming
375
+ # works - /tmp/ directories could be on another device.
376
+ with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index:
377
+ arg_list.append("--index-output=%s" % tmp_index)
378
+ arg_list.extend(treeish)
379
+
380
+ # Move the current index out of the way - otherwise the merge may fail as it
381
+ # considers existing entries. Moving it essentially clears the index.
382
+ # Unfortunately there is no 'soft' way to do it.
383
+ # The TemporaryFileSwap ensures the original file gets put back.
384
+ with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
385
+ repo.git.read_tree(*arg_list, **kwargs)
386
+ index = cls(repo, tmp_index)
387
+ index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file.
388
+ return index
389
+ # END index merge handling
390
+
391
+ # UTILITIES
392
+
393
+ @unbare_repo
394
+ def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
395
+ """Expand the directories in list of paths to the corresponding paths
396
+ accordingly.
397
+
398
+ :note:
399
+ git will add items multiple times even if a glob overlapped with manually
400
+ specified paths or if paths where specified multiple times - we respect that
401
+ and do not prune.
402
+ """
403
+
404
+ def raise_exc(e: Exception) -> NoReturn:
405
+ raise e
406
+
407
+ r = str(self.repo.working_tree_dir)
408
+ rs = r + os.sep
409
+ for path in paths:
410
+ abs_path = str(path)
411
+ if not osp.isabs(abs_path):
412
+ abs_path = osp.join(r, path)
413
+ # END make absolute path
414
+
415
+ try:
416
+ st = os.lstat(abs_path) # Handles non-symlinks as well.
417
+ except OSError:
418
+ # The lstat call may fail as the path may contain globs as well.
419
+ pass
420
+ else:
421
+ if S_ISLNK(st.st_mode):
422
+ yield abs_path.replace(rs, "")
423
+ continue
424
+ # END check symlink
425
+
426
+ # If the path is not already pointing to an existing file, resolve globs if possible.
427
+ if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
428
+ resolved_paths = glob.glob(abs_path)
429
+ # not abs_path in resolved_paths:
430
+ # A glob() resolving to the same path we are feeding it with is a
431
+ # glob() that failed to resolve. If we continued calling ourselves
432
+ # we'd endlessly recurse. If the condition below evaluates to true
433
+ # then we are likely dealing with a file whose name contains wildcard
434
+ # characters.
435
+ if abs_path not in resolved_paths:
436
+ for f in self._iter_expand_paths(glob.glob(abs_path)):
437
+ yield str(f).replace(rs, "")
438
+ continue
439
+ # END glob handling
440
+ try:
441
+ for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
442
+ for rela_file in files:
443
+ # Add relative paths only.
444
+ yield osp.join(root.replace(rs, ""), rela_file)
445
+ # END for each file in subdir
446
+ # END for each subdirectory
447
+ except OSError:
448
+ # It was a file or something that could not be iterated.
449
+ yield abs_path.replace(rs, "")
450
+ # END path exception handling
451
+ # END for each path
452
+
453
+ def _write_path_to_stdin(
454
+ self,
455
+ proc: "Popen",
456
+ filepath: PathLike,
457
+ item: PathLike,
458
+ fmakeexc: Callable[..., GitError],
459
+ fprogress: Callable[[PathLike, bool, PathLike], None],
460
+ read_from_stdout: bool = True,
461
+ ) -> Union[None, str]:
462
+ """Write path to ``proc.stdin`` and make sure it processes the item, including
463
+ progress.
464
+
465
+ :return:
466
+ stdout string
467
+
468
+ :param read_from_stdout:
469
+ If ``True``, ``proc.stdout`` will be read after the item was sent to stdin.
470
+ In that case, it will return ``None``.
471
+
472
+ :note:
473
+ There is a bug in :manpage:`git-update-index(1)` that prevents it from
474
+ sending reports just in time. This is why we have a version that tries to
475
+ read stdout and one which doesn't. In fact, the stdout is not important as
476
+ the piped-in files are processed anyway and just in time.
477
+
478
+ :note:
479
+ Newlines are essential here, git's behaviour is somewhat inconsistent on
480
+ this depending on the version, hence we try our best to deal with newlines
481
+ carefully. Usually the last newline will not be sent, instead we will close
482
+ stdin to break the pipe.
483
+ """
484
+ fprogress(filepath, False, item)
485
+ rval: Union[None, str] = None
486
+
487
+ if proc.stdin is not None:
488
+ try:
489
+ proc.stdin.write(("%s\n" % filepath).encode(defenc))
490
+ except IOError as e:
491
+ # Pipe broke, usually because some error happened.
492
+ raise fmakeexc() from e
493
+ # END write exception handling
494
+ proc.stdin.flush()
495
+
496
+ if read_from_stdout and proc.stdout is not None:
497
+ rval = proc.stdout.readline().strip()
498
+ fprogress(filepath, True, item)
499
+ return rval
500
+
501
+ def iter_blobs(
502
+ self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True
503
+ ) -> Iterator[Tuple[StageType, Blob]]:
504
+ """
505
+ :return:
506
+ Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and
507
+ stages, tuple(stage, Blob).
508
+
509
+ :param predicate:
510
+ Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
511
+ the iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you
512
+ to yield blobs only if they match a given list of paths.
513
+ """
514
+ for entry in self.entries.values():
515
+ blob = entry.to_blob(self.repo)
516
+ blob.size = entry.size
517
+ output = (entry.stage, blob)
518
+ if predicate(output):
519
+ yield output
520
+ # END for each entry
521
+
522
+ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
523
+ """
524
+ :return:
525
+ Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a
526
+ path in the index with a list containing sorted stage/blob pairs.
527
+
528
+ :note:
529
+ Blobs that have been removed in one side simply do not exist in the given
530
+ stage. That is, a file removed on the 'other' branch whose entries are at
531
+ stage 3 will not have a stage 3 entry.
532
+ """
533
+ is_unmerged_blob = lambda t: t[0] != 0
534
+ path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
535
+ for stage, blob in self.iter_blobs(is_unmerged_blob):
536
+ path_map.setdefault(blob.path, []).append((stage, blob))
537
+ # END for each unmerged blob
538
+ for line in path_map.values():
539
+ line.sort()
540
+
541
+ return path_map
542
+
543
+ @classmethod
544
+ def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]:
545
+ return entry_key(*entry)
546
+
547
+ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
548
+ """Resolve the blobs given in blob iterator.
549
+
550
+ This will effectively remove the index entries of the respective path at all
551
+ non-null stages and add the given blob as new stage null blob.
552
+
553
+ For each path there may only be one blob, otherwise a :exc:`ValueError` will be
554
+ raised claiming the path is already at stage 0.
555
+
556
+ :raise ValueError:
557
+ If one of the blobs already existed at stage 0.
558
+
559
+ :return:
560
+ self
561
+
562
+ :note:
563
+ You will have to write the index manually once you are done, i.e.
564
+ ``index.resolve_blobs(blobs).write()``.
565
+ """
566
+ for blob in iter_blobs:
567
+ stage_null_key = (blob.path, 0)
568
+ if stage_null_key in self.entries:
569
+ raise ValueError("Path %r already exists at stage 0" % str(blob.path))
570
+ # END assert blob is not stage 0 already
571
+
572
+ # Delete all possible stages.
573
+ for stage in (1, 2, 3):
574
+ try:
575
+ del self.entries[(blob.path, stage)]
576
+ except KeyError:
577
+ pass
578
+ # END ignore key errors
579
+ # END for each possible stage
580
+
581
+ self.entries[stage_null_key] = IndexEntry.from_blob(blob)
582
+ # END for each blob
583
+
584
+ return self
585
+
586
+ def update(self) -> "IndexFile":
587
+ """Reread the contents of our index file, discarding all cached information
588
+ we might have.
589
+
590
+ :note:
591
+ This is a possibly dangerous operations as it will discard your changes to
592
+ :attr:`index.entries <entries>`.
593
+
594
+ :return:
595
+ self
596
+ """
597
+ self._delete_entries_cache()
598
+ # Allows to lazily reread on demand.
599
+ return self
600
+
601
+ def write_tree(self) -> Tree:
602
+ """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
603
+ into the repository's object database and return it.
604
+
605
+ :return:
606
+ :class:`~git.objects.tree.Tree` object representing this index.
607
+
608
+ :note:
609
+ The tree will be written even if one or more objects the tree refers to does
610
+ not yet exist in the object database. This could happen if you added entries
611
+ to the index directly.
612
+
613
+ :raise ValueError:
614
+ If there are no entries in the cache.
615
+
616
+ :raise git.exc.UnmergedEntriesError:
617
+ """
618
+ # We obtain no lock as we just flush our contents to disk as tree.
619
+ # If we are a new index, the entries access will load our data accordingly.
620
+ mdb = MemoryDB()
621
+ entries = self._entries_sorted()
622
+ binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
623
+
624
+ # Copy changed trees only.
625
+ mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
626
+
627
+ # Note: Additional deserialization could be saved if write_tree_from_cache would
628
+ # return sorted tree entries.
629
+ root_tree = Tree(self.repo, binsha, path="")
630
+ root_tree._cache = tree_items
631
+ return root_tree
632
+
633
+ def _process_diff_args(
634
+ self,
635
+ args: List[Union[PathLike, "git_diff.Diffable"]],
636
+ ) -> List[Union[PathLike, "git_diff.Diffable"]]:
637
+ try:
638
+ args.pop(args.index(self))
639
+ except IndexError:
640
+ pass
641
+ # END remove self
642
+ return args
643
+
644
+ def _to_relative_path(self, path: PathLike) -> PathLike:
645
+ """
646
+ :return:
647
+ Version of path relative to our git directory or raise :exc:`ValueError` if
648
+ it is not within our git directory.
649
+
650
+ :raise ValueError:
651
+ """
652
+ if not osp.isabs(path):
653
+ return path
654
+ if self.repo.bare:
655
+ raise InvalidGitRepositoryError("require non-bare repository")
656
+ if not str(path).startswith(str(self.repo.working_tree_dir)):
657
+ raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
658
+ return os.path.relpath(path, self.repo.working_tree_dir)
659
+
660
+ def _preprocess_add_items(
661
+ self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]
662
+ ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
663
+ """Split the items into two lists of path strings and BaseEntries."""
664
+ paths = []
665
+ entries = []
666
+ # if it is a string put in list
667
+ if isinstance(items, (str, os.PathLike)):
668
+ items = [items]
669
+
670
+ for item in items:
671
+ if isinstance(item, (str, os.PathLike)):
672
+ paths.append(self._to_relative_path(item))
673
+ elif isinstance(item, (Blob, Submodule)):
674
+ entries.append(BaseIndexEntry.from_blob(item))
675
+ elif isinstance(item, BaseIndexEntry):
676
+ entries.append(item)
677
+ else:
678
+ raise TypeError("Invalid Type: %r" % item)
679
+ # END for each item
680
+ return paths, entries
681
+
682
+ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
683
+ """Store file at filepath in the database and return the base index entry.
684
+
685
+ :note:
686
+ This needs the :func:`~git.index.util.git_working_dir` decorator active!
687
+ This must be ensured in the calling code.
688
+ """
689
+ st = os.lstat(filepath) # Handles non-symlinks as well.
690
+ if S_ISLNK(st.st_mode):
691
+ # In PY3, readlink is a string, but we need bytes.
692
+ # In PY2, it was just OS encoded bytes, we assumed UTF-8.
693
+ open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
694
+ else:
695
+ open_stream = lambda: open(filepath, "rb")
696
+ with open_stream() as stream:
697
+ fprogress(filepath, False, filepath)
698
+ istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
699
+ fprogress(filepath, True, filepath)
700
+ return BaseIndexEntry(
701
+ (
702
+ stat_mode_to_index_mode(st.st_mode),
703
+ istream.binsha,
704
+ 0,
705
+ to_native_path_linux(filepath),
706
+ )
707
+ )
708
+
709
+ @unbare_repo
710
+ @git_working_dir
711
+ def _entries_for_paths(
712
+ self,
713
+ paths: List[str],
714
+ path_rewriter: Union[Callable, None],
715
+ fprogress: Callable,
716
+ entries: List[BaseIndexEntry],
717
+ ) -> List[BaseIndexEntry]:
718
+ entries_added: List[BaseIndexEntry] = []
719
+ if path_rewriter:
720
+ for path in paths:
721
+ if osp.isabs(path):
722
+ abspath = path
723
+ gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
724
+ else:
725
+ gitrelative_path = path
726
+ if self.repo.working_tree_dir:
727
+ abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
728
+ # END obtain relative and absolute paths
729
+
730
+ blob = Blob(
731
+ self.repo,
732
+ Blob.NULL_BIN_SHA,
733
+ stat_mode_to_index_mode(os.stat(abspath).st_mode),
734
+ to_native_path_linux(gitrelative_path),
735
+ )
736
+ # TODO: variable undefined
737
+ entries.append(BaseIndexEntry.from_blob(blob))
738
+ # END for each path
739
+ del paths[:]
740
+ # END rewrite paths
741
+
742
+ # HANDLE PATHS
743
+ assert len(entries_added) == 0
744
+ for filepath in self._iter_expand_paths(paths):
745
+ entries_added.append(self._store_path(filepath, fprogress))
746
+ # END for each filepath
747
+ # END path handling
748
+ return entries_added
749
+
750
+ def add(
751
+ self,
752
+ items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
753
+ force: bool = True,
754
+ fprogress: Callable = lambda *args: None,
755
+ path_rewriter: Union[Callable[..., PathLike], None] = None,
756
+ write: bool = True,
757
+ write_extension_data: bool = False,
758
+ ) -> List[BaseIndexEntry]:
759
+ R"""Add files from the working tree, specific blobs, or
760
+ :class:`~git.index.typ.BaseIndexEntry`\s to the index.
761
+
762
+ :param items:
763
+ Multiple types of items are supported, types can be mixed within one call.
764
+ Different types imply a different handling. File paths may generally be
765
+ relative or absolute.
766
+
767
+ - path string
768
+
769
+ Strings denote a relative or absolute path into the repository pointing
770
+ to an existing file, e.g., ``CHANGES``, `lib/myfile.ext``,
771
+ ``/home/gitrepo/lib/myfile.ext``.
772
+
773
+ Absolute paths must start with working tree directory of this index's
774
+ repository to be considered valid. For example, if it was initialized
775
+ with a non-normalized path, like ``/root/repo/../repo``, absolute paths
776
+ to be added must start with ``/root/repo/../repo``.
777
+
778
+ Paths provided like this must exist. When added, they will be written
779
+ into the object database.
780
+
781
+ PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
782
+ directories like ``lib``, which will add all the files within the
783
+ directory and subdirectories.
784
+
785
+ This equals a straight :manpage:`git-add(1)`.
786
+
787
+ They are added at stage 0.
788
+
789
+ - :class:~`git.objects.blob.Blob` or
790
+ :class:`~git.objects.submodule.base.Submodule` object
791
+
792
+ Blobs are added as they are assuming a valid mode is set.
793
+
794
+ The file they refer to may or may not exist in the file system, but must
795
+ be a path relative to our repository.
796
+
797
+ If their sha is null (40*0), their path must exist in the file system
798
+ relative to the git repository as an object will be created from the
799
+ data at the path.
800
+
801
+ The handling now very much equals the way string paths are processed,
802
+ except that the mode you have set will be kept. This allows you to
803
+ create symlinks by settings the mode respectively and writing the target
804
+ of the symlink directly into the file. This equals a default Linux
805
+ symlink which is not dereferenced automatically, except that it can be
806
+ created on filesystems not supporting it as well.
807
+
808
+ Please note that globs or directories are not allowed in
809
+ :class:`~git.objects.blob.Blob` objects.
810
+
811
+ They are added at stage 0.
812
+
813
+ - :class:`~git.index.typ.BaseIndexEntry` or type
814
+
815
+ Handling equals the one of :class:~`git.objects.blob.Blob` objects, but
816
+ the stage may be explicitly set. Please note that Index Entries require
817
+ binary sha's.
818
+
819
+ :param force:
820
+ **CURRENTLY INEFFECTIVE**
821
+ If ``True``, otherwise ignored or excluded files will be added anyway. As
822
+ opposed to the :manpage:`git-add(1)` command, we enable this flag by default
823
+ as the API user usually wants the item to be added even though they might be
824
+ excluded.
825
+
826
+ :param fprogress:
827
+ Function with signature ``f(path, done=False, item=item)`` called for each
828
+ path to be added, one time once it is about to be added where ``done=False``
829
+ and once after it was added where ``done=True``.
830
+
831
+ ``item`` is set to the actual item we handle, either a path or a
832
+ :class:`~git.index.typ.BaseIndexEntry`.
833
+
834
+ Please note that the processed path is not guaranteed to be present in the
835
+ index already as the index is currently being processed.
836
+
837
+ :param path_rewriter:
838
+ Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
839
+ for each passed entry which is the path to be actually recorded for the
840
+ object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
841
+ This allows you to write an index which is not identical to the layout of
842
+ the actual files on your hard-disk. If not ``None`` and `items` contain
843
+ plain paths, these paths will be converted to Entries beforehand and passed
844
+ to the path_rewriter. Please note that ``entry.path`` is relative to the git
845
+ repository.
846
+
847
+ :param write:
848
+ If ``True``, the index will be written once it was altered. Otherwise the
849
+ changes only exist in memory and are not available to git commands.
850
+
851
+ :param write_extension_data:
852
+ If ``True``, extension data will be written back to the index. This can lead
853
+ to issues in case it is containing the 'TREE' extension, which will cause
854
+ the :manpage:`git-commit(1)` command to write an old tree, instead of a new
855
+ one representing the now changed index.
856
+
857
+ This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
858
+ 'TREE' extension altogether. You should set it to ``True`` if you intend to
859
+ use :meth:`IndexFile.commit` exclusively while maintaining support for
860
+ third-party extensions. Besides that, you can usually safely ignore the
861
+ built-in extensions when using GitPython on repositories that are not
862
+ handled manually at all.
863
+
864
+ All current built-in extensions are listed here:
865
+ https://git-scm.com/docs/index-format
866
+
867
+ :return:
868
+ List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
869
+ just actually added.
870
+
871
+ :raise OSError:
872
+ If a supplied path did not exist. Please note that
873
+ :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
874
+ will be added even if their paths do not exist.
875
+ """
876
+ # Sort the entries into strings and Entries.
877
+ # Blobs are converted to entries automatically.
878
+ # Paths can be git-added. For everything else we use git-update-index.
879
+ paths, entries = self._preprocess_add_items(items)
880
+ entries_added: List[BaseIndexEntry] = []
881
+ # This code needs a working tree, so we try not to run it unless required.
882
+ # That way, we are OK on a bare repository as well.
883
+ # If there are no paths, the rewriter has nothing to do either.
884
+ if paths:
885
+ entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
886
+
887
+ # HANDLE ENTRIES
888
+ if entries:
889
+ null_mode_entries = [e for e in entries if e.mode == 0]
890
+ if null_mode_entries:
891
+ raise ValueError(
892
+ "At least one Entry has a null-mode - please use index.remove to remove files for clarity"
893
+ )
894
+ # END null mode should be remove
895
+
896
+ # HANDLE ENTRY OBJECT CREATION
897
+ # Create objects if required, otherwise go with the existing shas.
898
+ null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
899
+ if null_entries_indices:
900
+
901
+ @git_working_dir
902
+ def handle_null_entries(self: "IndexFile") -> None:
903
+ for ei in null_entries_indices:
904
+ null_entry = entries[ei]
905
+ new_entry = self._store_path(null_entry.path, fprogress)
906
+
907
+ # Update null entry.
908
+ entries[ei] = BaseIndexEntry(
909
+ (
910
+ null_entry.mode,
911
+ new_entry.binsha,
912
+ null_entry.stage,
913
+ null_entry.path,
914
+ )
915
+ )
916
+ # END for each entry index
917
+
918
+ # END closure
919
+
920
+ handle_null_entries(self)
921
+ # END null_entry handling
922
+
923
+ # REWRITE PATHS
924
+ # If we have to rewrite the entries, do so now, after we have generated all
925
+ # object sha's.
926
+ if path_rewriter:
927
+ for i, e in enumerate(entries):
928
+ entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
929
+ # END for each entry
930
+ # END handle path rewriting
931
+
932
+ # Just go through the remaining entries and provide progress info.
933
+ for i, entry in enumerate(entries):
934
+ progress_sent = i in null_entries_indices
935
+ if not progress_sent:
936
+ fprogress(entry.path, False, entry)
937
+ fprogress(entry.path, True, entry)
938
+ # END handle progress
939
+ # END for each entry
940
+ entries_added.extend(entries)
941
+ # END if there are base entries
942
+
943
+ # FINALIZE
944
+ # Add the new entries to this instance.
945
+ for entry in entries_added:
946
+ self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
947
+
948
+ if write:
949
+ self.write(ignore_extension_data=not write_extension_data)
950
+ # END handle write
951
+
952
+ return entries_added
953
+
954
+ def _items_to_rela_paths(
955
+ self,
956
+ items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
957
+ ) -> List[PathLike]:
958
+ """Returns a list of repo-relative paths from the given items which
959
+ may be absolute or relative paths, entries or blobs."""
960
+ paths = []
961
+ # If string, put in list.
962
+ if isinstance(items, (str, os.PathLike)):
963
+ items = [items]
964
+
965
+ for item in items:
966
+ if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
967
+ paths.append(self._to_relative_path(item.path))
968
+ elif isinstance(item, (str, os.PathLike)):
969
+ paths.append(self._to_relative_path(item))
970
+ else:
971
+ raise TypeError("Invalid item type: %r" % item)
972
+ # END for each item
973
+ return paths
974
+
975
+ @post_clear_cache
976
+ @default_index
977
+ def remove(
978
+ self,
979
+ items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
980
+ working_tree: bool = False,
981
+ **kwargs: Any,
982
+ ) -> List[str]:
983
+ R"""Remove the given items from the index and optionally from the working tree
984
+ as well.
985
+
986
+ :param items:
987
+ Multiple types of items are supported which may be be freely mixed.
988
+
989
+ - path string
990
+
991
+ Remove the given path at all stages. If it is a directory, you must
992
+ specify the ``r=True`` keyword argument to remove all file entries below
993
+ it. If absolute paths are given, they will be converted to a path
994
+ relative to the git repository directory containing the working tree
995
+
996
+ The path string may include globs, such as ``*.c``.
997
+
998
+ - :class:~`git.objects.blob.Blob` object
999
+
1000
+ Only the path portion is used in this case.
1001
+
1002
+ - :class:`~git.index.typ.BaseIndexEntry` or compatible type
1003
+
1004
+ The only relevant information here is the path. The stage is ignored.
1005
+
1006
+ :param working_tree:
1007
+ If ``True``, the entry will also be removed from the working tree,
1008
+ physically removing the respective file. This may fail if there are
1009
+ uncommitted changes in it.
1010
+
1011
+ :param kwargs:
1012
+ Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1013
+ ``r`` to allow recursive removal.
1014
+
1015
+ :return:
1016
+ List(path_string, ...) list of repository relative paths that have been
1017
+ removed effectively.
1018
+
1019
+ This is interesting to know in case you have provided a directory or globs.
1020
+ Paths are relative to the repository.
1021
+ """
1022
+ args = []
1023
+ if not working_tree:
1024
+ args.append("--cached")
1025
+ args.append("--")
1026
+
1027
+ # Preprocess paths.
1028
+ paths = self._items_to_rela_paths(items)
1029
+ removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1030
+
1031
+ # Process output to gain proper paths.
1032
+ # rm 'path'
1033
+ return [p[4:-1] for p in removed_paths]
1034
+
1035
+ @post_clear_cache
1036
+ @default_index
1037
+ def move(
1038
+ self,
1039
+ items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
1040
+ skip_errors: bool = False,
1041
+ **kwargs: Any,
1042
+ ) -> List[Tuple[str, str]]:
1043
+ """Rename/move the items, whereas the last item is considered the destination of
1044
+ the move operation.
1045
+
1046
+ If the destination is a file, the first item (of two) must be a file as well.
1047
+
1048
+ If the destination is a directory, it may be preceded by one or more directories
1049
+ or files.
1050
+
1051
+ The working tree will be affected in non-bare repositories.
1052
+
1053
+ :param items:
1054
+ Multiple types of items are supported, please see the :meth:`remove` method
1055
+ for reference.
1056
+
1057
+ :param skip_errors:
1058
+ If ``True``, errors such as ones resulting from missing source files will be
1059
+ skipped.
1060
+
1061
+ :param kwargs:
1062
+ Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
1063
+ ``dry_run`` or ``force``.
1064
+
1065
+ :return:
1066
+ List(tuple(source_path_string, destination_path_string), ...)
1067
+
1068
+ A list of pairs, containing the source file moved as well as its actual
1069
+ destination. Relative to the repository root.
1070
+
1071
+ :raise ValueError:
1072
+ If only one item was given.
1073
+
1074
+ :raise git.exc.GitCommandError:
1075
+ If git could not handle your request.
1076
+ """
1077
+ args = []
1078
+ if skip_errors:
1079
+ args.append("-k")
1080
+
1081
+ paths = self._items_to_rela_paths(items)
1082
+ if len(paths) < 2:
1083
+ raise ValueError("Please provide at least one source and one destination of the move operation")
1084
+
1085
+ was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
1086
+ kwargs["dry_run"] = True
1087
+
1088
+ # First execute rename in dry run so the command tells us what it actually does
1089
+ # (for later output).
1090
+ out = []
1091
+ mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1092
+
1093
+ # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
1094
+ # 'renaming' ones which we parse.
1095
+ for ln in range(int(len(mvlines) / 2), len(mvlines)):
1096
+ tokens = mvlines[ln].split(" to ")
1097
+ assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1098
+
1099
+ # [0] = Renaming x
1100
+ # [1] = y
1101
+ out.append((tokens[0][9:], tokens[1]))
1102
+ # END for each line to parse
1103
+
1104
+ # Either prepare for the real run, or output the dry-run result.
1105
+ if was_dry_run:
1106
+ return out
1107
+ # END handle dry run
1108
+
1109
+ # Now apply the actual operation.
1110
+ kwargs.pop("dry_run")
1111
+ self.repo.git.mv(args, paths, **kwargs)
1112
+
1113
+ return out
1114
+
1115
+ def commit(
1116
+ self,
1117
+ message: str,
1118
+ parent_commits: Union[List[Commit], None] = None,
1119
+ head: bool = True,
1120
+ author: Union[None, "Actor"] = None,
1121
+ committer: Union[None, "Actor"] = None,
1122
+ author_date: Union[datetime.datetime, str, None] = None,
1123
+ commit_date: Union[datetime.datetime, str, None] = None,
1124
+ skip_hooks: bool = False,
1125
+ ) -> Commit:
1126
+ """Commit the current default index file, creating a
1127
+ :class:`~git.objects.commit.Commit` object.
1128
+
1129
+ For more information on the arguments, see
1130
+ :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
1131
+
1132
+ :note:
1133
+ If you have manually altered the :attr:`entries` member of this instance,
1134
+ don't forget to :meth:`write` your changes to disk beforehand.
1135
+
1136
+ :note:
1137
+ Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
1138
+ ``--no-verify`` on the command line.
1139
+
1140
+ :return:
1141
+ :class:`~git.objects.commit.Commit` object representing the new commit
1142
+ """
1143
+ if not skip_hooks:
1144
+ run_commit_hook("pre-commit", self)
1145
+
1146
+ self._write_commit_editmsg(message)
1147
+ run_commit_hook("commit-msg", self, self._commit_editmsg_filepath())
1148
+ message = self._read_commit_editmsg()
1149
+ self._remove_commit_editmsg()
1150
+ tree = self.write_tree()
1151
+ rval = Commit.create_from_tree(
1152
+ self.repo,
1153
+ tree,
1154
+ message,
1155
+ parent_commits,
1156
+ head,
1157
+ author=author,
1158
+ committer=committer,
1159
+ author_date=author_date,
1160
+ commit_date=commit_date,
1161
+ )
1162
+ if not skip_hooks:
1163
+ run_commit_hook("post-commit", self)
1164
+ return rval
1165
+
1166
+ def _write_commit_editmsg(self, message: str) -> None:
1167
+ with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file:
1168
+ commit_editmsg_file.write(message.encode(defenc))
1169
+
1170
+ def _remove_commit_editmsg(self) -> None:
1171
+ os.remove(self._commit_editmsg_filepath())
1172
+
1173
+ def _read_commit_editmsg(self) -> str:
1174
+ with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file:
1175
+ return commit_editmsg_file.read().decode(defenc)
1176
+
1177
+ def _commit_editmsg_filepath(self) -> str:
1178
+ return osp.join(self.repo.common_dir, "COMMIT_EDITMSG")
1179
+
1180
+ def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes:
1181
+ stdin_IO = proc.stdin
1182
+ if stdin_IO:
1183
+ stdin_IO.flush()
1184
+ stdin_IO.close()
1185
+
1186
+ stdout = b""
1187
+ if not ignore_stdout and proc.stdout:
1188
+ stdout = proc.stdout.read()
1189
+
1190
+ if proc.stdout:
1191
+ proc.stdout.close()
1192
+ proc.wait()
1193
+ return stdout
1194
+
1195
+ @default_index
1196
+ def checkout(
1197
+ self,
1198
+ paths: Union[None, Iterable[PathLike]] = None,
1199
+ force: bool = False,
1200
+ fprogress: Callable = lambda *args: None,
1201
+ **kwargs: Any,
1202
+ ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1203
+ """Check out the given paths or all files from the version known to the index
1204
+ into the working tree.
1205
+
1206
+ :note:
1207
+ Be sure you have written pending changes using the :meth:`write` method in
1208
+ case you have altered the entries dictionary directly.
1209
+
1210
+ :param paths:
1211
+ If ``None``, all paths in the index will be checked out.
1212
+ Otherwise an iterable of relative or absolute paths or a single path
1213
+ pointing to files or directories in the index is expected.
1214
+
1215
+ :param force:
1216
+ If ``True``, existing files will be overwritten even if they contain local
1217
+ modifications.
1218
+ If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
1219
+
1220
+ :param fprogress:
1221
+ See :meth:`IndexFile.add` for signature and explanation.
1222
+
1223
+ The provided progress information will contain ``None`` as path and item if
1224
+ no explicit paths are given. Otherwise progress information will be send
1225
+ prior and after a file has been checked out.
1226
+
1227
+ :param kwargs:
1228
+ Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1229
+
1230
+ :return:
1231
+ Iterable yielding paths to files which have been checked out and are
1232
+ guaranteed to match the version stored in the index.
1233
+
1234
+ :raise git.exc.CheckoutError:
1235
+ * If at least one file failed to be checked out. This is a summary, hence it
1236
+ will checkout as many files as it can anyway.
1237
+ * If one of files or directories do not exist in the index (as opposed to
1238
+ the original git command, which ignores them).
1239
+
1240
+ :raise git.exc.GitCommandError:
1241
+ If error lines could not be parsed - this truly is an exceptional state.
1242
+
1243
+ :note:
1244
+ The checkout is limited to checking out the files in the index. Files which
1245
+ are not in the index anymore and exist in the working tree will not be
1246
+ deleted. This behaviour is fundamentally different to ``head.checkout``,
1247
+ i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1248
+ ``head.checkout`` instead of ``index.checkout``.
1249
+ """
1250
+ args = ["--index"]
1251
+ if force:
1252
+ args.append("--force")
1253
+
1254
+ failed_files = []
1255
+ failed_reasons = []
1256
+ unknown_lines = []
1257
+
1258
+ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None:
1259
+ stderr_IO = proc.stderr
1260
+ if not stderr_IO:
1261
+ return # Return early if stderr empty.
1262
+
1263
+ stderr_bytes = stderr_IO.read()
1264
+ # line contents:
1265
+ stderr = stderr_bytes.decode(defenc)
1266
+ # git-checkout-index: this already exists
1267
+ endings = (
1268
+ " already exists",
1269
+ " is not in the cache",
1270
+ " does not exist at stage",
1271
+ " is unmerged",
1272
+ )
1273
+ for line in stderr.splitlines():
1274
+ if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
1275
+ is_a_dir = " is a directory"
1276
+ unlink_issue = "unable to unlink old '"
1277
+ already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...)
1278
+ if line.endswith(is_a_dir):
1279
+ failed_files.append(line[: -len(is_a_dir)])
1280
+ failed_reasons.append(is_a_dir)
1281
+ elif line.startswith(unlink_issue):
1282
+ failed_files.append(line[len(unlink_issue) : line.rfind("'")])
1283
+ failed_reasons.append(unlink_issue)
1284
+ elif line.endswith(already_exists_issue):
1285
+ failed_files.append(line[: -len(already_exists_issue)])
1286
+ failed_reasons.append(already_exists_issue)
1287
+ else:
1288
+ unknown_lines.append(line)
1289
+ continue
1290
+ # END special lines parsing
1291
+
1292
+ for e in endings:
1293
+ if line.endswith(e):
1294
+ failed_files.append(line[20 : -len(e)])
1295
+ failed_reasons.append(e)
1296
+ break
1297
+ # END if ending matches
1298
+ # END for each possible ending
1299
+ # END for each line
1300
+ if unknown_lines:
1301
+ raise GitCommandError(("git-checkout-index",), 128, stderr)
1302
+ if failed_files:
1303
+ valid_files = list(set(iter_checked_out_files) - set(failed_files))
1304
+ raise CheckoutError(
1305
+ "Some files could not be checked out from the index due to local modifications",
1306
+ failed_files,
1307
+ valid_files,
1308
+ failed_reasons,
1309
+ )
1310
+
1311
+ # END stderr handler
1312
+
1313
+ if paths is None:
1314
+ args.append("--all")
1315
+ kwargs["as_process"] = 1
1316
+ fprogress(None, False, None)
1317
+ proc = self.repo.git.checkout_index(*args, **kwargs)
1318
+ proc.wait()
1319
+ fprogress(None, True, None)
1320
+ rval_iter = (e.path for e in self.entries.values())
1321
+ handle_stderr(proc, rval_iter)
1322
+ return rval_iter
1323
+ else:
1324
+ if isinstance(paths, str):
1325
+ paths = [paths]
1326
+
1327
+ # Make sure we have our entries loaded before we start checkout_index, which
1328
+ # will hold a lock on it. We try to get the lock as well during our entries
1329
+ # initialization.
1330
+ self.entries # noqa: B018
1331
+
1332
+ args.append("--stdin")
1333
+ kwargs["as_process"] = True
1334
+ kwargs["istream"] = subprocess.PIPE
1335
+ proc = self.repo.git.checkout_index(args, **kwargs)
1336
+ # FIXME: Reading from GIL!
1337
+ make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read())
1338
+ checked_out_files: List[PathLike] = []
1339
+
1340
+ for path in paths:
1341
+ co_path = to_native_path_linux(self._to_relative_path(path))
1342
+ # If the item is not in the index, it could be a directory.
1343
+ path_is_directory = False
1344
+
1345
+ try:
1346
+ self.entries[(co_path, 0)]
1347
+ except KeyError:
1348
+ folder = str(co_path)
1349
+ if not folder.endswith("/"):
1350
+ folder += "/"
1351
+ for entry in self.entries.values():
1352
+ if str(entry.path).startswith(folder):
1353
+ p = entry.path
1354
+ self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1355
+ checked_out_files.append(p)
1356
+ path_is_directory = True
1357
+ # END if entry is in directory
1358
+ # END for each entry
1359
+ # END path exception handlnig
1360
+
1361
+ if not path_is_directory:
1362
+ self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False)
1363
+ checked_out_files.append(co_path)
1364
+ # END path is a file
1365
+ # END for each path
1366
+ try:
1367
+ self._flush_stdin_and_wait(proc, ignore_stdout=True)
1368
+ except GitCommandError:
1369
+ # Without parsing stdout we don't know what failed.
1370
+ raise CheckoutError( # noqa: B904
1371
+ "Some files could not be checked out from the index, probably because they didn't exist.",
1372
+ failed_files,
1373
+ [],
1374
+ failed_reasons,
1375
+ )
1376
+
1377
+ handle_stderr(proc, checked_out_files)
1378
+ return checked_out_files
1379
+ # END paths handling
1380
+
1381
+ @default_index
1382
+ def reset(
1383
+ self,
1384
+ commit: Union[Commit, "Reference", str] = "HEAD",
1385
+ working_tree: bool = False,
1386
+ paths: Union[None, Iterable[PathLike]] = None,
1387
+ head: bool = False,
1388
+ **kwargs: Any,
1389
+ ) -> "IndexFile":
1390
+ """Reset the index to reflect the tree at the given commit. This will not adjust
1391
+ our HEAD reference by default, as opposed to
1392
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1393
+
1394
+ :param commit:
1395
+ Revision, :class:`~git.refs.reference.Reference` or
1396
+ :class:`~git.objects.commit.Commit` specifying the commit we should
1397
+ represent.
1398
+
1399
+ If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
1400
+ overwrite the default index.
1401
+
1402
+ :param working_tree:
1403
+ If ``True``, the files in the working tree will reflect the changed index.
1404
+ If ``False``, the working tree will not be touched.
1405
+ Please note that changes to the working copy will be discarded without
1406
+ warning!
1407
+
1408
+ :param head:
1409
+ If ``True``, the head will be set to the given commit. This is ``False`` by
1410
+ default, but if ``True``, this method behaves like
1411
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1412
+
1413
+ :param paths:
1414
+ If given as an iterable of absolute or repository-relative paths, only these
1415
+ will be reset to their state at the given commit-ish.
1416
+ The paths need to exist at the commit, otherwise an exception will be
1417
+ raised.
1418
+
1419
+ :param kwargs:
1420
+ Additional keyword arguments passed to :manpage:`git-reset(1)`.
1421
+
1422
+ :note:
1423
+ :meth:`IndexFile.reset`, as opposed to
1424
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
1425
+ order to maintain a consistent working tree. Instead, it will just check out
1426
+ the files according to their state in the index.
1427
+ If you want :manpage:`git-reset(1)`-like behaviour, use
1428
+ :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
1429
+
1430
+ :return:
1431
+ self
1432
+ """
1433
+ # What we actually want to do is to merge the tree into our existing index,
1434
+ # which is what git-read-tree does.
1435
+ new_inst = type(self).from_tree(self.repo, commit)
1436
+ if not paths:
1437
+ self.entries = new_inst.entries
1438
+ else:
1439
+ nie = new_inst.entries
1440
+ for path in paths:
1441
+ path = self._to_relative_path(path)
1442
+ try:
1443
+ key = entry_key(path, 0)
1444
+ self.entries[key] = nie[key]
1445
+ except KeyError:
1446
+ # If key is not in theirs, it musn't be in ours.
1447
+ try:
1448
+ del self.entries[key]
1449
+ except KeyError:
1450
+ pass
1451
+ # END handle deletion keyerror
1452
+ # END handle keyerror
1453
+ # END for each path
1454
+ # END handle paths
1455
+ self.write()
1456
+
1457
+ if working_tree:
1458
+ self.checkout(paths=paths, force=True)
1459
+ # END handle working tree
1460
+
1461
+ if head:
1462
+ self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit)
1463
+ # END handle head change
1464
+
1465
+ return self
1466
+
1467
+ # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1468
+ # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
1469
+ def diff(
1470
+ self,
1471
+ other: Union[ # type: ignore[override]
1472
+ Literal[git_diff.DiffConstants.INDEX],
1473
+ "Tree",
1474
+ "Commit",
1475
+ str,
1476
+ None,
1477
+ ] = git_diff.INDEX,
1478
+ paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
1479
+ create_patch: bool = False,
1480
+ **kwargs: Any,
1481
+ ) -> git_diff.DiffIndex:
1482
+ """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
1483
+ or :class:`~git.objects.commit.Commit` object.
1484
+
1485
+ For documentation of the parameters and return values, see
1486
+ :meth:`Diffable.diff <git.diff.Diffable.diff>`.
1487
+
1488
+ :note:
1489
+ Will only work with indices that represent the default git index as they
1490
+ have not been initialized with a stream.
1491
+ """
1492
+ # Only run if we are the default repository index.
1493
+ if self._file_path != self._index_path():
1494
+ raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
1495
+ # Index against index is always empty.
1496
+ if other is self.INDEX:
1497
+ return git_diff.DiffIndex()
1498
+
1499
+ # Index against anything but None is a reverse diff with the respective item.
1500
+ # Handle existing -R flags properly.
1501
+ # Transform strings to the object so that we can call diff on it.
1502
+ if isinstance(other, str):
1503
+ other = self.repo.rev_parse(other)
1504
+ # END object conversion
1505
+
1506
+ if isinstance(other, Object): # For Tree or Commit.
1507
+ # Invert the existing R flag.
1508
+ cur_val = kwargs.get("R", False)
1509
+ kwargs["R"] = not cur_val
1510
+ return other.diff(self.INDEX, paths, create_patch, **kwargs)
1511
+ # END diff against other item handling
1512
+
1513
+ # If other is not None here, something is wrong.
1514
+ if other is not None:
1515
+ raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
1516
+
1517
+ # Diff against working copy - can be handled by superclass natively.
1518
+ return super().diff(other, paths, create_patch, **kwargs)
ILYA/Lib/site-packages/git/index/fun.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This module is part of GitPython and is released under the
2
+ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
+
4
+ """Standalone functions to accompany the index implementation and make it more
5
+ versatile."""
6
+
7
+ __all__ = [
8
+ "write_cache",
9
+ "read_cache",
10
+ "write_tree_from_cache",
11
+ "entry_key",
12
+ "stat_mode_to_index_mode",
13
+ "S_IFGITLINK",
14
+ "run_commit_hook",
15
+ "hook_path",
16
+ ]
17
+
18
+ from io import BytesIO
19
+ import os
20
+ import os.path as osp
21
+ from pathlib import Path
22
+ from stat import S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_ISDIR, S_ISLNK, S_IXUSR
23
+ import subprocess
24
+ import sys
25
+
26
+ from gitdb.base import IStream
27
+ from gitdb.typ import str_tree_type
28
+
29
+ from git.cmd import handle_process_output, safer_popen
30
+ from git.compat import defenc, force_bytes, force_text, safe_decode
31
+ from git.exc import HookExecutionError, UnmergedEntriesError
32
+ from git.objects.fun import (
33
+ traverse_tree_recursive,
34
+ traverse_trees_recursive,
35
+ tree_to_stream,
36
+ )
37
+ from git.util import IndexFileSHA1Writer, finalize_process
38
+
39
+ from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
40
+ from .util import pack, unpack
41
+
42
+ # typing -----------------------------------------------------------------------------
43
+
44
+ from typing import Dict, IO, List, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast
45
+
46
+ from git.types import PathLike
47
+
48
+ if TYPE_CHECKING:
49
+ from git.db import GitCmdObjectDB
50
+ from git.objects.tree import TreeCacheTup
51
+
52
+ from .base import IndexFile
53
+
54
+ # ------------------------------------------------------------------------------------
55
+
56
+ S_IFGITLINK = S_IFLNK | S_IFDIR
57
+ """Flags for a submodule."""
58
+
59
+ CE_NAMEMASK_INV = ~CE_NAMEMASK
60
+
61
+
62
+ def hook_path(name: str, git_dir: PathLike) -> str:
63
+ """:return: path to the given named hook in the given git repository directory"""
64
+ return osp.join(git_dir, "hooks", name)
65
+
66
+
67
+ def _has_file_extension(path: str) -> str:
68
+ return osp.splitext(path)[1]
69
+
70
+
71
+ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
72
+ """Run the commit hook of the given name. Silently ignore hooks that do not exist.
73
+
74
+ :param name:
75
+ Name of hook, like ``pre-commit``.
76
+
77
+ :param index:
78
+ :class:`~git.index.base.IndexFile` instance.
79
+
80
+ :param args:
81
+ Arguments passed to hook file.
82
+
83
+ :raise git.exc.HookExecutionError:
84
+ """
85
+ hp = hook_path(name, index.repo.git_dir)
86
+ if not os.access(hp, os.X_OK):
87
+ return
88
+
89
+ env = os.environ.copy()
90
+ env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
91
+ env["GIT_EDITOR"] = ":"
92
+ cmd = [hp]
93
+ try:
94
+ if sys.platform == "win32" and not _has_file_extension(hp):
95
+ # Windows only uses extensions to determine how to open files
96
+ # (doesn't understand shebangs). Try using bash to run the hook.
97
+ relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
98
+ cmd = ["bash.exe", relative_hp]
99
+
100
+ process = safer_popen(
101
+ cmd + list(args),
102
+ env=env,
103
+ stdout=subprocess.PIPE,
104
+ stderr=subprocess.PIPE,
105
+ cwd=index.repo.working_dir,
106
+ )
107
+ except Exception as ex:
108
+ raise HookExecutionError(hp, ex) from ex
109
+ else:
110
+ stdout_list: List[str] = []
111
+ stderr_list: List[str] = []
112
+ handle_process_output(process, stdout_list.append, stderr_list.append, finalize_process)
113
+ stdout = "".join(stdout_list)
114
+ stderr = "".join(stderr_list)
115
+ if process.returncode != 0:
116
+ stdout = force_text(stdout, defenc)
117
+ stderr = force_text(stderr, defenc)
118
+ raise HookExecutionError(hp, process.returncode, stderr, stdout)
119
+ # END handle return code
120
+
121
+
122
+ def stat_mode_to_index_mode(mode: int) -> int:
123
+ """Convert the given mode from a stat call to the corresponding index mode and
124
+ return it."""
125
+ if S_ISLNK(mode): # symlinks
126
+ return S_IFLNK
127
+ if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK: # submodules
128
+ return S_IFGITLINK
129
+ return S_IFREG | (mode & S_IXUSR and 0o755 or 0o644) # blobs with or without executable bit
130
+
131
+
132
+ def write_cache(
133
+ entries: Sequence[Union[BaseIndexEntry, "IndexEntry"]],
134
+ stream: IO[bytes],
135
+ extension_data: Union[None, bytes] = None,
136
+ ShaStreamCls: Type[IndexFileSHA1Writer] = IndexFileSHA1Writer,
137
+ ) -> None:
138
+ """Write the cache represented by entries to a stream.
139
+
140
+ :param entries:
141
+ **Sorted** list of entries.
142
+
143
+ :param stream:
144
+ Stream to wrap into the AdapterStreamCls - it is used for final output.
145
+
146
+ :param ShaStreamCls:
147
+ Type to use when writing to the stream. It produces a sha while writing to it,
148
+ before the data is passed on to the wrapped stream.
149
+
150
+ :param extension_data:
151
+ Any kind of data to write as a trailer, it must begin a 4 byte identifier,
152
+ followed by its size (4 bytes).
153
+ """
154
+ # Wrap the stream into a compatible writer.
155
+ stream_sha = ShaStreamCls(stream)
156
+
157
+ tell = stream_sha.tell
158
+ write = stream_sha.write
159
+
160
+ # Header
161
+ version = 2
162
+ write(b"DIRC")
163
+ write(pack(">LL", version, len(entries)))
164
+
165
+ # Body
166
+ for entry in entries:
167
+ beginoffset = tell()
168
+ write(entry.ctime_bytes) # ctime
169
+ write(entry.mtime_bytes) # mtime
170
+ path_str = str(entry.path)
171
+ path: bytes = force_bytes(path_str, encoding=defenc)
172
+ plen = len(path) & CE_NAMEMASK # Path length
173
+ assert plen == len(path), "Path %s too long to fit into index" % entry.path
174
+ flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values.
175
+ write(
176
+ pack(
177
+ ">LLLLLL20sH",
178
+ entry.dev,
179
+ entry.inode,
180
+ entry.mode,
181
+ entry.uid,
182
+ entry.gid,
183
+ entry.size,
184
+ entry.binsha,
185
+ flags,
186
+ )
187
+ )
188
+ write(path)
189
+ real_size = (tell() - beginoffset + 8) & ~7
190
+ write(b"\0" * ((beginoffset + real_size) - tell()))
191
+ # END for each entry
192
+
193
+ # Write previously cached extensions data.
194
+ if extension_data is not None:
195
+ stream_sha.write(extension_data)
196
+
197
+ # Write the sha over the content.
198
+ stream_sha.write_sha()
199
+
200
+
201
+ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
202
+ """Return tuple(version_long, num_entries) from the given stream."""
203
+ type_id = stream.read(4)
204
+ if type_id != b"DIRC":
205
+ raise AssertionError("Invalid index file header: %r" % type_id)
206
+ unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2)))
207
+ version, num_entries = unpacked
208
+
209
+ # TODO: Handle version 3: extended data, see read-cache.c.
210
+ assert version in (1, 2)
211
+ return version, num_entries
212
+
213
+
214
+ def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]:
215
+ """
216
+ :return:
217
+ Key suitable to be used for the
218
+ :attr:`index.entries <git.index.base.IndexFile.entries>` dictionary.
219
+
220
+ :param entry:
221
+ One instance of type BaseIndexEntry or the path and the stage.
222
+ """
223
+
224
+ # def is_entry_key_tup(entry_key: Tuple) -> TypeGuard[Tuple[PathLike, int]]:
225
+ # return isinstance(entry_key, tuple) and len(entry_key) == 2
226
+
227
+ if len(entry) == 1:
228
+ entry_first = entry[0]
229
+ assert isinstance(entry_first, BaseIndexEntry)
230
+ return (entry_first.path, entry_first.stage)
231
+ else:
232
+ # assert is_entry_key_tup(entry)
233
+ entry = cast(Tuple[PathLike, int], entry)
234
+ return entry
235
+ # END handle entry
236
+
237
+
238
+ def read_cache(
239
+ stream: IO[bytes],
240
+ ) -> Tuple[int, Dict[Tuple[PathLike, int], "IndexEntry"], bytes, bytes]:
241
+ """Read a cache file from the given stream.
242
+
243
+ :return:
244
+ tuple(version, entries_dict, extension_data, content_sha)
245
+
246
+ * *version* is the integer version number.
247
+ * *entries_dict* is a dictionary which maps IndexEntry instances to a path at a
248
+ stage.
249
+ * *extension_data* is ``""`` or 4 bytes of type + 4 bytes of size + size bytes.
250
+ * *content_sha* is a 20 byte sha on all cache file contents.
251
+ """
252
+ version, num_entries = read_header(stream)
253
+ count = 0
254
+ entries: Dict[Tuple[PathLike, int], "IndexEntry"] = {}
255
+
256
+ read = stream.read
257
+ tell = stream.tell
258
+ while count < num_entries:
259
+ beginoffset = tell()
260
+ ctime = unpack(">8s", read(8))[0]
261
+ mtime = unpack(">8s", read(8))[0]
262
+ (dev, ino, mode, uid, gid, size, sha, flags) = unpack(">LLLLLL20sH", read(20 + 4 * 6 + 2))
263
+ path_size = flags & CE_NAMEMASK
264
+ path = read(path_size).decode(defenc)
265
+
266
+ real_size = (tell() - beginoffset + 8) & ~7
267
+ read((beginoffset + real_size) - tell())
268
+ entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size))
269
+ # entry_key would be the method to use, but we save the effort.
270
+ entries[(path, entry.stage)] = entry
271
+ count += 1
272
+ # END for each entry
273
+
274
+ # The footer contains extension data and a sha on the content so far.
275
+ # Keep the extension footer,and verify we have a sha in the end.
276
+ # Extension data format is:
277
+ # 4 bytes ID
278
+ # 4 bytes length of chunk
279
+ # Repeated 0 - N times
280
+ extension_data = stream.read(~0)
281
+ assert len(extension_data) > 19, (
282
+ "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data)
283
+ )
284
+
285
+ content_sha = extension_data[-20:]
286
+
287
+ # Truncate the sha in the end as we will dynamically create it anyway.
288
+ extension_data = extension_data[:-20]
289
+
290
+ return (version, entries, extension_data, content_sha)
291
+
292
+
293
+ def write_tree_from_cache(
294
+ entries: List[IndexEntry], odb: "GitCmdObjectDB", sl: slice, si: int = 0
295
+ ) -> Tuple[bytes, List["TreeCacheTup"]]:
296
+ R"""Create a tree from the given sorted list of entries and put the respective
297
+ trees into the given object database.
298
+
299
+ :param entries:
300
+ **Sorted** list of :class:`~git.index.typ.IndexEntry`\s.
301
+
302
+ :param odb:
303
+ Object database to store the trees in.
304
+
305
+ :param si:
306
+ Start index at which we should start creating subtrees.
307
+
308
+ :param sl:
309
+ Slice indicating the range we should process on the entries list.
310
+
311
+ :return:
312
+ tuple(binsha, list(tree_entry, ...))
313
+
314
+ A tuple of a sha and a list of tree entries being a tuple of hexsha, mode, name.
315
+ """
316
+ tree_items: List["TreeCacheTup"] = []
317
+
318
+ ci = sl.start
319
+ end = sl.stop
320
+ while ci < end:
321
+ entry = entries[ci]
322
+ if entry.stage != 0:
323
+ raise UnmergedEntriesError(entry)
324
+ # END abort on unmerged
325
+ ci += 1
326
+ rbound = entry.path.find("/", si)
327
+ if rbound == -1:
328
+ # It's not a tree.
329
+ tree_items.append((entry.binsha, entry.mode, entry.path[si:]))
330
+ else:
331
+ # Find common base range.
332
+ base = entry.path[si:rbound]
333
+ xi = ci
334
+ while xi < end:
335
+ oentry = entries[xi]
336
+ orbound = oentry.path.find("/", si)
337
+ if orbound == -1 or oentry.path[si:orbound] != base:
338
+ break
339
+ # END abort on base mismatch
340
+ xi += 1
341
+ # END find common base
342
+
343
+ # Enter recursion.
344
+ # ci - 1 as we want to count our current item as well.
345
+ sha, _tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)
346
+ tree_items.append((sha, S_IFDIR, base))
347
+
348
+ # Skip ahead.
349
+ ci = xi
350
+ # END handle bounds
351
+ # END for each entry
352
+
353
+ # Finally create the tree.
354
+ sio = BytesIO()
355
+ tree_to_stream(tree_items, sio.write) # Writes to stream as bytes, but doesn't change tree_items.
356
+ sio.seek(0)
357
+
358
+ istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio))
359
+ return (istream.binsha, tree_items)
360
+
361
+
362
+ def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> BaseIndexEntry:
363
+ return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2]))
364
+
365
+
366
+ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]:
367
+ R"""
368
+ :return:
369
+ List of :class:`~git.index.typ.BaseIndexEntry`\s representing the aggressive
370
+ merge of the given trees. All valid entries are on stage 0, whereas the
371
+ conflicting ones are left on stage 1, 2 or 3, whereas stage 1 corresponds to the
372
+ common ancestor tree, 2 to our tree and 3 to 'their' tree.
373
+
374
+ :param tree_shas:
375
+ 1, 2 or 3 trees as identified by their binary 20 byte shas. If 1 or two, the
376
+ entries will effectively correspond to the last given tree. If 3 are given, a 3
377
+ way merge is performed.
378
+ """
379
+ out: List[BaseIndexEntry] = []
380
+
381
+ # One and two way is the same for us, as we don't have to handle an existing
382
+ # index, instrea
383
+ if len(tree_shas) in (1, 2):
384
+ for entry in traverse_tree_recursive(odb, tree_shas[-1], ""):
385
+ out.append(_tree_entry_to_baseindexentry(entry, 0))
386
+ # END for each entry
387
+ return out
388
+ # END handle single tree
389
+
390
+ if len(tree_shas) > 3:
391
+ raise ValueError("Cannot handle %i trees at once" % len(tree_shas))
392
+
393
+ # Three trees.
394
+ for base, ours, theirs in traverse_trees_recursive(odb, tree_shas, ""):
395
+ if base is not None:
396
+ # Base version exists.
397
+ if ours is not None:
398
+ # Ours exists.
399
+ if theirs is not None:
400
+ # It exists in all branches. Ff it was changed in both
401
+ # its a conflict. Otherwise, we take the changed version.
402
+ # This should be the most common branch, so it comes first.
403
+ if (base[0] != ours[0] and base[0] != theirs[0] and ours[0] != theirs[0]) or (
404
+ base[1] != ours[1] and base[1] != theirs[1] and ours[1] != theirs[1]
405
+ ):
406
+ # Changed by both.
407
+ out.append(_tree_entry_to_baseindexentry(base, 1))
408
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
409
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
410
+ elif base[0] != ours[0] or base[1] != ours[1]:
411
+ # Only we changed it.
412
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
413
+ else:
414
+ # Either nobody changed it, or they did. In either
415
+ # case, use theirs.
416
+ out.append(_tree_entry_to_baseindexentry(theirs, 0))
417
+ # END handle modification
418
+ else:
419
+ if ours[0] != base[0] or ours[1] != base[1]:
420
+ # They deleted it, we changed it, conflict.
421
+ out.append(_tree_entry_to_baseindexentry(base, 1))
422
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
423
+ # else:
424
+ # # We didn't change it, ignore.
425
+ # pass
426
+ # END handle our change
427
+ # END handle theirs
428
+ else:
429
+ if theirs is None:
430
+ # Deleted in both, its fine - it's out.
431
+ pass
432
+ else:
433
+ if theirs[0] != base[0] or theirs[1] != base[1]:
434
+ # Deleted in ours, changed theirs, conflict.
435
+ out.append(_tree_entry_to_baseindexentry(base, 1))
436
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
437
+ # END theirs changed
438
+ # else:
439
+ # # Theirs didn't change.
440
+ # pass
441
+ # END handle theirs
442
+ # END handle ours
443
+ else:
444
+ # All three can't be None.
445
+ if ours is None:
446
+ # Added in their branch.
447
+ assert theirs is not None
448
+ out.append(_tree_entry_to_baseindexentry(theirs, 0))
449
+ elif theirs is None:
450
+ # Added in our branch.
451
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
452
+ else:
453
+ # Both have it, except for the base, see whether it changed.
454
+ if ours[0] != theirs[0] or ours[1] != theirs[1]:
455
+ out.append(_tree_entry_to_baseindexentry(ours, 2))
456
+ out.append(_tree_entry_to_baseindexentry(theirs, 3))
457
+ else:
458
+ # It was added the same in both.
459
+ out.append(_tree_entry_to_baseindexentry(ours, 0))
460
+ # END handle two items
461
+ # END handle heads
462
+ # END handle base exists
463
+ # END for each entries tuple
464
+
465
+ return out
ILYA/Lib/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
+ )
ILYA/Lib/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
ILYA/Lib/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
ILYA/Lib/site-packages/git/objects/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (873 Bytes). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/base.cpython-311.pyc ADDED
Binary file (13.3 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/blob.cpython-311.pyc ADDED
Binary file (1.85 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/commit.cpython-311.pyc ADDED
Binary file (35.1 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/fun.cpython-311.pyc ADDED
Binary file (9.5 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/tag.cpython-311.pyc ADDED
Binary file (5.49 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/tree.cpython-311.pyc ADDED
Binary file (19.1 kB). View file
 
ILYA/Lib/site-packages/git/objects/__pycache__/util.cpython-311.pyc ADDED
Binary file (30.4 kB). View file
 
ILYA/Lib/site-packages/git/objects/base.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2008, 2009 Michael Trier ([email protected]) 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")