Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/AUTHORS +59 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/INSTALLER +1 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/LICENSE +29 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/METADATA +295 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/RECORD +83 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/REQUESTED +0 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/WHEEL +5 -0
- venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/top_level.txt +1 -0
- venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/RECORD +175 -0
- venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/WHEEL +5 -0
- venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE +202 -0
- venv/lib/python3.10/site-packages/git/__init__.py +300 -0
- venv/lib/python3.10/site-packages/git/__pycache__/__init__.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/cmd.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/compat.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/config.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/db.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/diff.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/exc.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/remote.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/types.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/__pycache__/util.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/cmd.py +1724 -0
- venv/lib/python3.10/site-packages/git/compat.py +165 -0
- venv/lib/python3.10/site-packages/git/config.py +944 -0
- venv/lib/python3.10/site-packages/git/db.py +71 -0
- venv/lib/python3.10/site-packages/git/diff.py +775 -0
- venv/lib/python3.10/site-packages/git/exc.py +228 -0
- venv/lib/python3.10/site-packages/git/index/typ.py +202 -0
- venv/lib/python3.10/site-packages/git/index/util.py +121 -0
- venv/lib/python3.10/site-packages/git/objects/__init__.py +25 -0
- venv/lib/python3.10/site-packages/git/objects/__pycache__/base.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/__pycache__/fun.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/__pycache__/tag.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/__pycache__/tree.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/base.py +301 -0
- venv/lib/python3.10/site-packages/git/objects/blob.py +48 -0
- venv/lib/python3.10/site-packages/git/objects/commit.py +909 -0
- venv/lib/python3.10/site-packages/git/objects/fun.py +281 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/__init__.py +7 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/__init__.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/base.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/root.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/util.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/base.py +1632 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/root.py +467 -0
- venv/lib/python3.10/site-packages/git/objects/submodule/util.py +121 -0
- venv/lib/python3.10/site-packages/git/objects/tag.py +140 -0
- venv/lib/python3.10/site-packages/git/objects/tree.py +414 -0
- venv/lib/python3.10/site-packages/git/objects/util.py +700 -0
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/AUTHORS
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GitPython was originally written by Michael Trier.
|
| 2 |
+
GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich.
|
| 3 |
+
|
| 4 |
+
Contributors are:
|
| 5 |
+
|
| 6 |
+
-Michael Trier <mtrier _at_ gmail.com>
|
| 7 |
+
-Alan Briolat
|
| 8 |
+
-Florian Apolloner <florian _at_ apolloner.eu>
|
| 9 |
+
-David Aguilar <davvid _at_ gmail.com>
|
| 10 |
+
-Jelmer Vernooij <jelmer _at_ samba.org>
|
| 11 |
+
-Steve Frécinaux <code _at_ istique.net>
|
| 12 |
+
-Kai Lautaportti <kai _at_ lautaportti.fi>
|
| 13 |
+
-Paul Sowden <paul _at_ idontsmoke.co.uk>
|
| 14 |
+
-Sebastian Thiel <byronimo _at_ gmail.com>
|
| 15 |
+
-Jonathan Chu <jonathan.chu _at_ me.com>
|
| 16 |
+
-Vincent Driessen <me _at_ nvie.com>
|
| 17 |
+
-Phil Elson <pelson _dot_ pub _at_ gmail.com>
|
| 18 |
+
-Bernard `Guyzmo` Pratz <guyzmo+gitpython+pub@m0g.net>
|
| 19 |
+
-Timothy B. Hartman <tbhartman _at_ gmail.com>
|
| 20 |
+
-Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
|
| 21 |
+
-Peter Jones <pjones _at_ redhat.com>
|
| 22 |
+
-Anson Mansfield <anson.mansfield _at_ gmail.com>
|
| 23 |
+
-Ken Odegard <ken.odegard _at_ gmail.com>
|
| 24 |
+
-Alexis Horgix Chotard
|
| 25 |
+
-Piotr Babij <piotr.babij _at_ gmail.com>
|
| 26 |
+
-Mikuláš Poul <mikulaspoul _at_ gmail.com>
|
| 27 |
+
-Charles Bouchard-Légaré <cblegare.atl _at_ ntis.ca>
|
| 28 |
+
-Yaroslav Halchenko <debian _at_ onerussian.com>
|
| 29 |
+
-Tim Swast <swast _at_ google.com>
|
| 30 |
+
-William Luc Ritchie
|
| 31 |
+
-David Host <hostdm _at_ outlook.com>
|
| 32 |
+
-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
|
| 33 |
+
-Steven Whitman <ninloot _at_ gmail.com>
|
| 34 |
+
-Stefan Stancu <stefan.stancu _at_ gmail.com>
|
| 35 |
+
-César Izurieta <cesar _at_ caih.org>
|
| 36 |
+
-Arthur Milchior <arthur _at_ milchior.fr>
|
| 37 |
+
-Anil Khatri <anil.soccer.khatri _at_ gmail.com>
|
| 38 |
+
-JJ Graham <thetwoj _at_ gmail.com>
|
| 39 |
+
-Ben Thayer <ben _at_ benthayer.com>
|
| 40 |
+
-Dries Kennes <admin _at_ dries007.net>
|
| 41 |
+
-Pratik Anurag <panurag247365 _at_ gmail.com>
|
| 42 |
+
-Harmon <harmon.public _at_ gmail.com>
|
| 43 |
+
-Liam Beguin <liambeguin _at_ gmail.com>
|
| 44 |
+
-Ram Rachum <ram _at_ rachum.com>
|
| 45 |
+
-Alba Mendez <me _at_ alba.sh>
|
| 46 |
+
-Robert Westman <robert _at_ byteflux.io>
|
| 47 |
+
-Hugo van Kemenade
|
| 48 |
+
-Hiroki Tokunaga <tokusan441 _at_ gmail.com>
|
| 49 |
+
-Julien Mauroy <pro.julien.mauroy _at_ gmail.com>
|
| 50 |
+
-Patrick Gerard
|
| 51 |
+
-Luke Twist <itsluketwist@gmail.com>
|
| 52 |
+
-Joseph Hale <me _at_ jhale.dev>
|
| 53 |
+
-Santos Gallegos <stsewd _at_ proton.me>
|
| 54 |
+
-Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
|
| 55 |
+
-Eliah Kagan <eliah.kagan _at_ gmail.com>
|
| 56 |
+
-Ethan Lin <et.repositories _at_ gmail.com>
|
| 57 |
+
-Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
|
| 58 |
+
|
| 59 |
+
Portions derived from other open source works and are clearly marked.
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (C) 2008, 2009 Michael Trier and contributors
|
| 2 |
+
All rights reserved.
|
| 3 |
+
|
| 4 |
+
Redistribution and use in source and binary forms, with or without
|
| 5 |
+
modification, are permitted provided that the following conditions
|
| 6 |
+
are met:
|
| 7 |
+
|
| 8 |
+
* Redistributions of source code must retain the above copyright
|
| 9 |
+
notice, this list of conditions and the following disclaimer.
|
| 10 |
+
|
| 11 |
+
* Redistributions in binary form must reproduce the above copyright
|
| 12 |
+
notice, this list of conditions and the following disclaimer in the
|
| 13 |
+
documentation and/or other materials provided with the distribution.
|
| 14 |
+
|
| 15 |
+
* Neither the name of the GitPython project nor the names of
|
| 16 |
+
its contributors may be used to endorse or promote products derived
|
| 17 |
+
from this software without specific prior written permission.
|
| 18 |
+
|
| 19 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| 20 |
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| 21 |
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| 22 |
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| 23 |
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| 24 |
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
| 25 |
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
| 26 |
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
| 27 |
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
| 28 |
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 29 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/METADATA
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: GitPython
|
| 3 |
+
Version: 3.1.44
|
| 4 |
+
Summary: GitPython is a Python library used to interact with Git repositories
|
| 5 |
+
Home-page: https://github.com/gitpython-developers/GitPython
|
| 6 |
+
Author: Sebastian Thiel, Michael Trier
|
| 7 |
+
Author-email: byronimo@gmail.com, mtrier@gmail.com
|
| 8 |
+
License: BSD-3-Clause
|
| 9 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 10 |
+
Classifier: Environment :: Console
|
| 11 |
+
Classifier: Intended Audience :: Developers
|
| 12 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 13 |
+
Classifier: Operating System :: OS Independent
|
| 14 |
+
Classifier: Operating System :: POSIX
|
| 15 |
+
Classifier: Operating System :: Microsoft :: Windows
|
| 16 |
+
Classifier: Operating System :: MacOS :: MacOS X
|
| 17 |
+
Classifier: Typing :: Typed
|
| 18 |
+
Classifier: Programming Language :: Python
|
| 19 |
+
Classifier: Programming Language :: Python :: 3
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 25 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 26 |
+
Requires-Python: >=3.7
|
| 27 |
+
Description-Content-Type: text/markdown
|
| 28 |
+
License-File: LICENSE
|
| 29 |
+
License-File: AUTHORS
|
| 30 |
+
Requires-Dist: gitdb<5,>=4.0.1
|
| 31 |
+
Requires-Dist: typing-extensions>=3.7.4.3; python_version < "3.8"
|
| 32 |
+
Provides-Extra: test
|
| 33 |
+
Requires-Dist: coverage[toml]; extra == "test"
|
| 34 |
+
Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
|
| 35 |
+
Requires-Dist: mock; python_version < "3.8" and extra == "test"
|
| 36 |
+
Requires-Dist: mypy; extra == "test"
|
| 37 |
+
Requires-Dist: pre-commit; extra == "test"
|
| 38 |
+
Requires-Dist: pytest>=7.3.1; extra == "test"
|
| 39 |
+
Requires-Dist: pytest-cov; extra == "test"
|
| 40 |
+
Requires-Dist: pytest-instafail; extra == "test"
|
| 41 |
+
Requires-Dist: pytest-mock; extra == "test"
|
| 42 |
+
Requires-Dist: pytest-sugar; extra == "test"
|
| 43 |
+
Requires-Dist: typing-extensions; python_version < "3.11" and extra == "test"
|
| 44 |
+
Provides-Extra: doc
|
| 45 |
+
Requires-Dist: sphinx<7.2,>=7.1.2; extra == "doc"
|
| 46 |
+
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
| 47 |
+
Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
|
| 48 |
+
|
| 49 |
+

|
| 50 |
+
[](https://readthedocs.org/projects/gitpython/?badge=stable)
|
| 51 |
+
[](https://repology.org/metapackage/python:gitpython/versions)
|
| 52 |
+
|
| 53 |
+
## [Gitoxide](https://github.com/Byron/gitoxide): A peek into the future…
|
| 54 |
+
|
| 55 |
+
I started working on GitPython in 2009, back in the days when Python was 'my thing' and I had great plans with it.
|
| 56 |
+
Of course, back in the days, I didn't really know what I was doing and this shows in many places. Somewhat similar to
|
| 57 |
+
Python this happens to be 'good enough', but at the same time is deeply flawed and broken beyond repair.
|
| 58 |
+
|
| 59 |
+
By now, GitPython is widely used and I am sure there is a good reason for that, it's something to be proud of and happy about.
|
| 60 |
+
The community is maintaining the software and is keeping it relevant for which I am absolutely grateful. For the time to come I am happy to continue maintaining GitPython, remaining hopeful that one day it won't be needed anymore.
|
| 61 |
+
|
| 62 |
+
More than 15 years after my first meeting with 'git' I am still in excited about it, and am happy to finally have the tools and
|
| 63 |
+
probably the skills to scratch that itch of mine: implement `git` in a way that makes tool creation a piece of cake for most.
|
| 64 |
+
|
| 65 |
+
If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
|
| 66 |
+
implementation of 'git' in [Rust](https://www.rust-lang.org).
|
| 67 |
+
|
| 68 |
+
*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)*
|
| 69 |
+
|
| 70 |
+
## GitPython
|
| 71 |
+
|
| 72 |
+
GitPython is a python library used to interact with git repositories, high-level like git-porcelain,
|
| 73 |
+
or low-level like git-plumbing.
|
| 74 |
+
|
| 75 |
+
It provides abstractions of git objects for easy access of repository data often backed by calling the `git`
|
| 76 |
+
command-line program.
|
| 77 |
+
|
| 78 |
+
### DEVELOPMENT STATUS
|
| 79 |
+
|
| 80 |
+
This project is in **maintenance mode**, which means that
|
| 81 |
+
|
| 82 |
+
- …there will be no feature development, unless these are contributed
|
| 83 |
+
- …there will be no bug fixes, unless they are relevant to the safety of users, or contributed
|
| 84 |
+
- …issues will be responded to with waiting times of up to a month
|
| 85 |
+
|
| 86 |
+
The project is open to contributions of all kinds, as well as new maintainers.
|
| 87 |
+
|
| 88 |
+
### REQUIREMENTS
|
| 89 |
+
|
| 90 |
+
GitPython needs the `git` executable to be installed on the system and available in your
|
| 91 |
+
`PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it
|
| 92 |
+
by setting the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
|
| 93 |
+
|
| 94 |
+
- Git (1.7.x or newer)
|
| 95 |
+
- Python >= 3.7
|
| 96 |
+
|
| 97 |
+
The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
|
| 98 |
+
The installer takes care of installing them for you.
|
| 99 |
+
|
| 100 |
+
### INSTALL
|
| 101 |
+
|
| 102 |
+
GitPython and its required package dependencies can be installed in any of the following ways, all of which should typically be done in a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
| 103 |
+
|
| 104 |
+
#### From PyPI
|
| 105 |
+
|
| 106 |
+
To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run:
|
| 107 |
+
|
| 108 |
+
```sh
|
| 109 |
+
pip install GitPython
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
(A distribution package can also be downloaded for manual installation at [the PyPI page](https://pypi.org/project/GitPython/).)
|
| 113 |
+
|
| 114 |
+
#### From downloaded source code
|
| 115 |
+
|
| 116 |
+
If you have downloaded the source code, run this from inside the unpacked `GitPython` directory:
|
| 117 |
+
|
| 118 |
+
```sh
|
| 119 |
+
pip install .
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
#### By cloning the source code repository
|
| 123 |
+
|
| 124 |
+
To clone the [the GitHub repository](https://github.com/gitpython-developers/GitPython) from source to work on the code, you can do it like so:
|
| 125 |
+
|
| 126 |
+
```sh
|
| 127 |
+
git clone https://github.com/gitpython-developers/GitPython
|
| 128 |
+
cd GitPython
|
| 129 |
+
./init-tests-after-clone.sh
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
|
| 133 |
+
|
| 134 |
+
If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`):
|
| 135 |
+
|
| 136 |
+
```sh
|
| 137 |
+
gh repo clone GitPython
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
| 141 |
+
|
| 142 |
+
Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs):
|
| 143 |
+
|
| 144 |
+
```sh
|
| 145 |
+
pip install -e ".[test]"
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
In the less common case that you do not want to install test dependencies, `pip install -e .` can be used instead.
|
| 149 |
+
|
| 150 |
+
#### With editable *dependencies* (not preferred, and rarely needed)
|
| 151 |
+
|
| 152 |
+
In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediately reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
|
| 153 |
+
|
| 154 |
+
If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
|
| 155 |
+
|
| 156 |
+
```sh
|
| 157 |
+
pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
The submodules must have been cloned for that to work, but that will already be the case if you have run `./init-tests-after-clone.sh`. You can use `pip list` to check which packages are installed editably and which are installed normally.
|
| 161 |
+
|
| 162 |
+
To reiterate, this approach should only rarely be used. For most development it is preferable to allow the gitdb and smmap dependencices to be retrieved automatically from PyPI in their latest stable packaged versions.
|
| 163 |
+
|
| 164 |
+
### Limitations
|
| 165 |
+
|
| 166 |
+
#### Leakage of System Resources
|
| 167 |
+
|
| 168 |
+
GitPython is not suited for long-running processes (like daemons) as it tends to
|
| 169 |
+
leak system resources. It was written in a time where destructors (as implemented
|
| 170 |
+
in the `__del__` method) still ran deterministically.
|
| 171 |
+
|
| 172 |
+
In case you still want to use it in such a context, you will want to search the
|
| 173 |
+
codebase for `__del__` implementations and call these yourself when you see fit.
|
| 174 |
+
|
| 175 |
+
Another way assure proper cleanup of resources is to factor out GitPython into a
|
| 176 |
+
separate process which can be dropped periodically.
|
| 177 |
+
|
| 178 |
+
#### Windows support
|
| 179 |
+
|
| 180 |
+
See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525).
|
| 181 |
+
|
| 182 |
+
### RUNNING TESTS
|
| 183 |
+
|
| 184 |
+
_Important_: Right after cloning this repository, please be sure to have executed
|
| 185 |
+
the `./init-tests-after-clone.sh` script in the repository root. Otherwise
|
| 186 |
+
you will encounter test failures.
|
| 187 |
+
|
| 188 |
+
#### Install test dependencies
|
| 189 |
+
|
| 190 |
+
Ensure testing libraries are installed. This is taken care of already if you installed with:
|
| 191 |
+
|
| 192 |
+
```sh
|
| 193 |
+
pip install -e ".[test]"
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
If you had installed with a command like `pip install -e .` instead, you can still run
|
| 197 |
+
the above command to add the testing dependencies.
|
| 198 |
+
|
| 199 |
+
#### Test commands
|
| 200 |
+
|
| 201 |
+
To test, run:
|
| 202 |
+
|
| 203 |
+
```sh
|
| 204 |
+
pytest
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
To lint, and apply some linting fixes as well as automatic code formatting, run:
|
| 208 |
+
|
| 209 |
+
```sh
|
| 210 |
+
pre-commit run --all-files
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
This includes the linting and autoformatting done by Ruff, as well as some other checks.
|
| 214 |
+
|
| 215 |
+
To typecheck, run:
|
| 216 |
+
|
| 217 |
+
```sh
|
| 218 |
+
mypy
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
#### CI (and tox)
|
| 222 |
+
|
| 223 |
+
Style and formatting checks, and running tests on all the different supported Python versions, will be performed:
|
| 224 |
+
|
| 225 |
+
- Upon submitting a pull request.
|
| 226 |
+
- On each push, *if* you have a fork with GitHub Actions enabled.
|
| 227 |
+
- Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
|
| 228 |
+
|
| 229 |
+
#### Configuration files
|
| 230 |
+
|
| 231 |
+
Specific tools are all configured in the `./pyproject.toml` file:
|
| 232 |
+
|
| 233 |
+
- `pytest` (test runner)
|
| 234 |
+
- `coverage.py` (code coverage)
|
| 235 |
+
- `ruff` (linter and formatter)
|
| 236 |
+
- `mypy` (type checker)
|
| 237 |
+
|
| 238 |
+
Orchestration tools:
|
| 239 |
+
|
| 240 |
+
- Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file.
|
| 241 |
+
- Configuration for `tox` is in `./tox.ini`.
|
| 242 |
+
- Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`.
|
| 243 |
+
|
| 244 |
+
### Contributions
|
| 245 |
+
|
| 246 |
+
Please have a look at the [contributions file][contributing].
|
| 247 |
+
|
| 248 |
+
### INFRASTRUCTURE
|
| 249 |
+
|
| 250 |
+
- [User Documentation](http://gitpython.readthedocs.org)
|
| 251 |
+
- [Questions and Answers](http://stackexchange.com/filters/167317/gitpython)
|
| 252 |
+
- Please post on Stack Overflow and use the `gitpython` tag
|
| 253 |
+
- [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues)
|
| 254 |
+
- Post reproducible bugs and feature requests as a new issue.
|
| 255 |
+
Please be sure to provide the following information if posting bugs:
|
| 256 |
+
- GitPython version (e.g. `import git; git.__version__`)
|
| 257 |
+
- Python version (e.g. `python --version`)
|
| 258 |
+
- The encountered stack-trace, if applicable
|
| 259 |
+
- Enough information to allow reproducing the issue
|
| 260 |
+
|
| 261 |
+
### How to make a new release
|
| 262 |
+
|
| 263 |
+
1. Update/verify the **version** in the `VERSION` file.
|
| 264 |
+
2. Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
|
| 265 |
+
3. Commit everything.
|
| 266 |
+
4. Run `git tag -s <version>` to tag the version in Git.
|
| 267 |
+
5. _Optionally_ create and activate a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment). (Then the next step can install `build` and `twine`.)
|
| 268 |
+
6. Run `make release`.
|
| 269 |
+
7. Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog.
|
| 270 |
+
|
| 271 |
+
### Projects using GitPython
|
| 272 |
+
|
| 273 |
+
- [PyDriller](https://github.com/ishepard/pydriller)
|
| 274 |
+
- [Kivy Designer](https://github.com/kivy/kivy-designer)
|
| 275 |
+
- [Prowl](https://github.com/nettitude/Prowl)
|
| 276 |
+
- [Python Taint](https://github.com/python-security/pyt)
|
| 277 |
+
- [Buster](https://github.com/axitkhurana/buster)
|
| 278 |
+
- [git-ftp](https://github.com/ezyang/git-ftp)
|
| 279 |
+
- [Git-Pandas](https://github.com/wdm0006/git-pandas)
|
| 280 |
+
- [PyGitUp](https://github.com/msiemens/PyGitUp)
|
| 281 |
+
- [PyJFuzz](https://github.com/mseclab/PyJFuzz)
|
| 282 |
+
- [Loki](https://github.com/Neo23x0/Loki)
|
| 283 |
+
- [Omniwallet](https://github.com/OmniLayer/omniwallet)
|
| 284 |
+
- [GitViper](https://github.com/BeayemX/GitViper)
|
| 285 |
+
- [Git Gud](https://github.com/bthayer2365/git-gud)
|
| 286 |
+
|
| 287 |
+
### LICENSE
|
| 288 |
+
|
| 289 |
+
[3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
|
| 290 |
+
|
| 291 |
+
One file exclusively used for fuzz testing is subject to [a separate license, detailed here](./fuzzing/README.md#license).
|
| 292 |
+
This file is not included in the wheel or sdist packages published by the maintainers of GitPython.
|
| 293 |
+
|
| 294 |
+
[contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
|
| 295 |
+
[license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/RECORD
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GitPython-3.1.44.dist-info/AUTHORS,sha256=tZ9LuyBks2V2HKTPK7kCmtd9Guu_LyU1oZHvU0NiAok,2334
|
| 2 |
+
GitPython-3.1.44.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
GitPython-3.1.44.dist-info/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
|
| 4 |
+
GitPython-3.1.44.dist-info/METADATA,sha256=0O_Fr2Y7A-DlPYhlbSxGjblBC2mWkw3USNUhyL80Ip8,13245
|
| 5 |
+
GitPython-3.1.44.dist-info/RECORD,,
|
| 6 |
+
GitPython-3.1.44.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 7 |
+
GitPython-3.1.44.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
| 8 |
+
GitPython-3.1.44.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
|
| 9 |
+
git/__init__.py,sha256=nkQImgv-bWdiZOFDjzN-gbt93FoRHD0nY6_t9LQxy4Y,8899
|
| 10 |
+
git/__pycache__/__init__.cpython-310.pyc,,
|
| 11 |
+
git/__pycache__/cmd.cpython-310.pyc,,
|
| 12 |
+
git/__pycache__/compat.cpython-310.pyc,,
|
| 13 |
+
git/__pycache__/config.cpython-310.pyc,,
|
| 14 |
+
git/__pycache__/db.cpython-310.pyc,,
|
| 15 |
+
git/__pycache__/diff.cpython-310.pyc,,
|
| 16 |
+
git/__pycache__/exc.cpython-310.pyc,,
|
| 17 |
+
git/__pycache__/remote.cpython-310.pyc,,
|
| 18 |
+
git/__pycache__/types.cpython-310.pyc,,
|
| 19 |
+
git/__pycache__/util.cpython-310.pyc,,
|
| 20 |
+
git/cmd.py,sha256=QwiaBy0mFbi9xjRKhRgUVK-_-K6xVdFqh9l0cxPqPSc,67724
|
| 21 |
+
git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
|
| 22 |
+
git/config.py,sha256=vTUlK6d8ORqFqjOv4Vbq_Hm-5mp-jOAt1dkq0IdzJ3U,34933
|
| 23 |
+
git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
|
| 24 |
+
git/diff.py,sha256=wmpMCIdMiVOqreGVPOGYyO4gFboGOAicyrvvI7PPjEg,27095
|
| 25 |
+
git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
|
| 26 |
+
git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
|
| 27 |
+
git/index/__pycache__/__init__.cpython-310.pyc,,
|
| 28 |
+
git/index/__pycache__/base.cpython-310.pyc,,
|
| 29 |
+
git/index/__pycache__/fun.cpython-310.pyc,,
|
| 30 |
+
git/index/__pycache__/typ.cpython-310.pyc,,
|
| 31 |
+
git/index/__pycache__/util.cpython-310.pyc,,
|
| 32 |
+
git/index/base.py,sha256=nDD7XVLNbgBKpJMrrTVyHBy6NVLWgDkk7oUw6ZOegPc,60808
|
| 33 |
+
git/index/fun.py,sha256=37cA3DBC9vpAnSVu5TGA072SnoF5XZOkOukExwlejHs,16736
|
| 34 |
+
git/index/typ.py,sha256=uuKNwitUw83FhVaLSwo4pY7PHDQudtZTLJrLGym4jcI,6570
|
| 35 |
+
git/index/util.py,sha256=fULi7GPG-MvprKrRCD5c15GNdzku_1E38We0d97WB3A,3659
|
| 36 |
+
git/objects/__init__.py,sha256=O6ZL_olX7e5-8iIbKviRPkVSJxN37WA-EC0q9d48U5Y,637
|
| 37 |
+
git/objects/__pycache__/__init__.cpython-310.pyc,,
|
| 38 |
+
git/objects/__pycache__/base.cpython-310.pyc,,
|
| 39 |
+
git/objects/__pycache__/blob.cpython-310.pyc,,
|
| 40 |
+
git/objects/__pycache__/commit.cpython-310.pyc,,
|
| 41 |
+
git/objects/__pycache__/fun.cpython-310.pyc,,
|
| 42 |
+
git/objects/__pycache__/tag.cpython-310.pyc,,
|
| 43 |
+
git/objects/__pycache__/tree.cpython-310.pyc,,
|
| 44 |
+
git/objects/__pycache__/util.cpython-310.pyc,,
|
| 45 |
+
git/objects/base.py,sha256=0dqNkSRVH0mk0-7ZKIkGBK7iNYrzLTVxwQFUd6CagsE,10277
|
| 46 |
+
git/objects/blob.py,sha256=zwwq0KfOMYeP5J2tW5CQatoLyeqFRlfkxP1Vwx1h07s,1215
|
| 47 |
+
git/objects/commit.py,sha256=GH1_83C9t7RGTukwozTHDgvxYQPRjTHhPDkXJyBbJyo,30553
|
| 48 |
+
git/objects/fun.py,sha256=B4jCqhAjm6Hl79GK58FPzW1H9K6Wc7Tx0rssyWmAcEE,8935
|
| 49 |
+
git/objects/submodule/__init__.py,sha256=6xySp767LVz3UylWgUalntS_nGXRuVzXxDuFAv_Wc2c,303
|
| 50 |
+
git/objects/submodule/__pycache__/__init__.cpython-310.pyc,,
|
| 51 |
+
git/objects/submodule/__pycache__/base.cpython-310.pyc,,
|
| 52 |
+
git/objects/submodule/__pycache__/root.cpython-310.pyc,,
|
| 53 |
+
git/objects/submodule/__pycache__/util.cpython-310.pyc,,
|
| 54 |
+
git/objects/submodule/base.py,sha256=MQ-2xV8JznGwy2hLQv1aeQNgAkhBhgc5tdtClFL3DmE,63901
|
| 55 |
+
git/objects/submodule/root.py,sha256=5eTtYNHasqdPq6q0oDCPr7IaO6uAHL3b4DxMoiO2LhE,20246
|
| 56 |
+
git/objects/submodule/util.py,sha256=sQqAYaiSJdFkZa9NlAuK_wTsMNiS-kkQnQjvIoJtc_o,3509
|
| 57 |
+
git/objects/tag.py,sha256=jAGESnpmTEv-dLakPzheT5ILZFFArcItnXYqfxfDrgc,4441
|
| 58 |
+
git/objects/tree.py,sha256=jJH888SHiP4dGzE-ra1yenQOyya_0C_MkHr06c1gHpM,13849
|
| 59 |
+
git/objects/util.py,sha256=Nlza4zLgdPmr_Yasyvvs6c1rKtW_wMxI6wDmQpQ3ufw,23846
|
| 60 |
+
git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 61 |
+
git/refs/__init__.py,sha256=DWlJNnsx-4jM_E-VycbP-FZUdn6iWhjnH_uZ_pZXBro,509
|
| 62 |
+
git/refs/__pycache__/__init__.cpython-310.pyc,,
|
| 63 |
+
git/refs/__pycache__/head.cpython-310.pyc,,
|
| 64 |
+
git/refs/__pycache__/log.cpython-310.pyc,,
|
| 65 |
+
git/refs/__pycache__/reference.cpython-310.pyc,,
|
| 66 |
+
git/refs/__pycache__/remote.cpython-310.pyc,,
|
| 67 |
+
git/refs/__pycache__/symbolic.cpython-310.pyc,,
|
| 68 |
+
git/refs/__pycache__/tag.cpython-310.pyc,,
|
| 69 |
+
git/refs/head.py,sha256=SGa3N301HfAi79X6UR5Mcg7mO9TnCH3Bk549kHlJVaQ,10513
|
| 70 |
+
git/refs/log.py,sha256=kXiuAgTo1DIuM_BfbDUk9gQ0YO-mutIMVdHv1_ES90o,12493
|
| 71 |
+
git/refs/reference.py,sha256=l6mhF4YLSEwtjz6b9PpOQH-fkng7EYWMaJhkjn-2jXA,5630
|
| 72 |
+
git/refs/remote.py,sha256=WwqV9T7BbYf3F_WZNUQivu9xktIIKGklCjDpwQrhD-A,2806
|
| 73 |
+
git/refs/symbolic.py,sha256=c8zOwaqzcg-J-rGrpuWdvh8zwMvSUqAHghd4vJoYG_s,34552
|
| 74 |
+
git/refs/tag.py,sha256=kgzV2vhpL4FD2TqHb0BJuMRAHgAvJF-TcoyWlaB-djQ,5010
|
| 75 |
+
git/remote.py,sha256=pYn9dAlz-QwvNMWXD1M57pMPQitthOM86qTRK_cpTqU,46786
|
| 76 |
+
git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
|
| 77 |
+
git/repo/__pycache__/__init__.cpython-310.pyc,,
|
| 78 |
+
git/repo/__pycache__/base.cpython-310.pyc,,
|
| 79 |
+
git/repo/__pycache__/fun.cpython-310.pyc,,
|
| 80 |
+
git/repo/base.py,sha256=0GU6nKNdT8SYjDI5Y5DeZ1zCEX3tHeq1VW2MSpne05g,59891
|
| 81 |
+
git/repo/fun.py,sha256=HSGC0-rqeKKx9fDg7JyQyMZgIwUWn-FnSZR_gRGpG-E,13573
|
| 82 |
+
git/types.py,sha256=MQzIDEOnoueXGsAJF_0MgUc_osH7Eu0Sw3DQofYzCVE,10272
|
| 83 |
+
git/util.py,sha256=2uAv34zZ_827-zJ3-D5ACrVH-4Q4EO_KLUTH23zi2AI,43770
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/REQUESTED
ADDED
|
File without changes
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (75.6.0)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
venv/lib/python3.10/site-packages/GitPython-3.1.44.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
git
|
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/RECORD
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cryptography-45.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
cryptography-45.0.5.dist-info/METADATA,sha256=Dn2Ls7PhdM88VKgRTBFDskPTSNeGHm7SezAcdIPYdKc,5690
|
| 3 |
+
cryptography-45.0.5.dist-info/RECORD,,
|
| 4 |
+
cryptography-45.0.5.dist-info/WHEEL,sha256=VqNc6WOTGHZndw6FKXcyBqGvpOjDTrsbnnH79sUpcvY,107
|
| 5 |
+
cryptography-45.0.5.dist-info/licenses/LICENSE,sha256=Pgx8CRqUi4JTO6mP18u0BDLW8amsv4X1ki0vmak65rs,197
|
| 6 |
+
cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
|
| 7 |
+
cryptography-45.0.5.dist-info/licenses/LICENSE.BSD,sha256=YCxMdILeZHndLpeTzaJ15eY9dz2s0eymiSMqtwCPtPs,1532
|
| 8 |
+
cryptography/__about__.py,sha256=X8cr9wNd4PDeKHYUt6ppLOYb-V56WYSgLBuYKlivuBY,445
|
| 9 |
+
cryptography/__init__.py,sha256=QDuQ3te_14R9iGF-2Q5yEwD1FTJrCWqpnuhlMIJBsno,762
|
| 10 |
+
cryptography/__pycache__/__about__.cpython-310.pyc,,
|
| 11 |
+
cryptography/__pycache__/__init__.cpython-310.pyc,,
|
| 12 |
+
cryptography/__pycache__/exceptions.cpython-310.pyc,,
|
| 13 |
+
cryptography/__pycache__/fernet.cpython-310.pyc,,
|
| 14 |
+
cryptography/__pycache__/utils.cpython-310.pyc,,
|
| 15 |
+
cryptography/exceptions.py,sha256=835EWILc2fwxw-gyFMriciC2SqhViETB10LBSytnDIc,1087
|
| 16 |
+
cryptography/fernet.py,sha256=3Cvxkh0KJSbX8HbnCHu4wfCW7U0GgfUA3v_qQ8a8iWc,6963
|
| 17 |
+
cryptography/hazmat/__init__.py,sha256=5IwrLWrVp0AjEr_4FdWG_V057NSJGY_W4egNNsuct0g,455
|
| 18 |
+
cryptography/hazmat/__pycache__/__init__.cpython-310.pyc,,
|
| 19 |
+
cryptography/hazmat/__pycache__/_oid.cpython-310.pyc,,
|
| 20 |
+
cryptography/hazmat/_oid.py,sha256=7-aiQLWCYT7FDrQ0sUWBNYkN5ADEGTZwLQE_ZCksPxs,16795
|
| 21 |
+
cryptography/hazmat/backends/__init__.py,sha256=O5jvKFQdZnXhKeqJ-HtulaEL9Ni7mr1mDzZY5kHlYhI,361
|
| 22 |
+
cryptography/hazmat/backends/__pycache__/__init__.cpython-310.pyc,,
|
| 23 |
+
cryptography/hazmat/backends/openssl/__init__.py,sha256=p3jmJfnCag9iE5sdMrN6VvVEu55u46xaS_IjoI0SrmA,305
|
| 24 |
+
cryptography/hazmat/backends/openssl/__pycache__/__init__.cpython-310.pyc,,
|
| 25 |
+
cryptography/hazmat/backends/openssl/__pycache__/backend.cpython-310.pyc,,
|
| 26 |
+
cryptography/hazmat/backends/openssl/backend.py,sha256=ev-Prba_u1bCMk3ZWspkdOuLBbDlf_7wJnHK6xdYs7c,10329
|
| 27 |
+
cryptography/hazmat/bindings/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
| 28 |
+
cryptography/hazmat/bindings/__pycache__/__init__.cpython-310.pyc,,
|
| 29 |
+
cryptography/hazmat/bindings/_rust.abi3.so,sha256=BtscyLV0h1_Wy8zPS9g0OAG4KOJsnqLVG1w4ZqZnr5s,12446840
|
| 30 |
+
cryptography/hazmat/bindings/_rust/__init__.pyi,sha256=KhqLhXFPArPzzJ7DYO9Fl8FoXB_BagAd_r4Dm_Ze9Xo,1257
|
| 31 |
+
cryptography/hazmat/bindings/_rust/_openssl.pyi,sha256=mpNJLuYLbCVrd5i33FBTmWwL_55Dw7JPkSLlSX9Q7oI,230
|
| 32 |
+
cryptography/hazmat/bindings/_rust/asn1.pyi,sha256=BrGjC8J6nwuS-r3EVcdXJB8ndotfY9mbQYOfpbPG0HA,354
|
| 33 |
+
cryptography/hazmat/bindings/_rust/exceptions.pyi,sha256=exXr2xw_0pB1kk93cYbM3MohbzoUkjOms1ZMUi0uQZE,640
|
| 34 |
+
cryptography/hazmat/bindings/_rust/ocsp.pyi,sha256=VPVWuKHI9EMs09ZLRYAGvR0Iz0mCMmEzXAkgJHovpoM,4020
|
| 35 |
+
cryptography/hazmat/bindings/_rust/openssl/__init__.pyi,sha256=iOAMDyHoNwwCSZfZzuXDr64g4GpGUeDgEN-LjXqdrBM,1522
|
| 36 |
+
cryptography/hazmat/bindings/_rust/openssl/aead.pyi,sha256=4Nddw6-ynzIB3w2W86WvkGKTLlTDk_6F5l54RHCuy3E,2688
|
| 37 |
+
cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi,sha256=LhPzHWSXJq4grAJXn6zSvSSdV-aYIIscHDwIPlJGGPs,1315
|
| 38 |
+
cryptography/hazmat/bindings/_rust/openssl/cmac.pyi,sha256=nPH0X57RYpsAkRowVpjQiHE566ThUTx7YXrsadmrmHk,564
|
| 39 |
+
cryptography/hazmat/bindings/_rust/openssl/dh.pyi,sha256=Z3TC-G04-THtSdAOPLM1h2G7ml5bda1ElZUcn5wpuhk,1564
|
| 40 |
+
cryptography/hazmat/bindings/_rust/openssl/dsa.pyi,sha256=qBtkgj2albt2qFcnZ9UDrhzoNhCVO7HTby5VSf1EXMI,1299
|
| 41 |
+
cryptography/hazmat/bindings/_rust/openssl/ec.pyi,sha256=zJy0pRa5n-_p2dm45PxECB_-B6SVZyNKfjxFDpPqT38,1691
|
| 42 |
+
cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi,sha256=VXfXd5G6hUivg399R1DYdmW3eTb0EebzDTqjRC2gaRw,532
|
| 43 |
+
cryptography/hazmat/bindings/_rust/openssl/ed448.pyi,sha256=Yx49lqdnjsD7bxiDV1kcaMrDktug5evi5a6zerMiy2s,514
|
| 44 |
+
cryptography/hazmat/bindings/_rust/openssl/hashes.pyi,sha256=OWZvBx7xfo_HJl41Nc--DugVyCVPIprZ3HlOPTSWH9g,984
|
| 45 |
+
cryptography/hazmat/bindings/_rust/openssl/hmac.pyi,sha256=BXZn7NDjL3JAbYW0SQ8pg1iyC5DbQXVhUAiwsi8DFR8,702
|
| 46 |
+
cryptography/hazmat/bindings/_rust/openssl/kdf.pyi,sha256=4FGpmCR2H8SVGoe4SUX7h5wzhyvuRi8hnzKKfvnJJrw,1379
|
| 47 |
+
cryptography/hazmat/bindings/_rust/openssl/keys.pyi,sha256=teIt8M6ZEMJrn4s3W0UnW0DZ-30Jd68WnSsKKG124l0,912
|
| 48 |
+
cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi,sha256=_SW9NtQ5FDlAbdclFtWpT4lGmxKIKHpN-4j8J2BzYfQ,585
|
| 49 |
+
cryptography/hazmat/bindings/_rust/openssl/rsa.pyi,sha256=2OQCNSXkxgc-3uw1xiCCloIQTV6p9_kK79Yu0rhZgPc,1364
|
| 50 |
+
cryptography/hazmat/bindings/_rust/openssl/x25519.pyi,sha256=ewn4GpQyb7zPwE-ni7GtyQgMC0A1mLuqYsSyqv6nI_s,523
|
| 51 |
+
cryptography/hazmat/bindings/_rust/openssl/x448.pyi,sha256=juTZTmli8jO_5Vcufg-vHvx_tCyezmSLIh_9PU3TczI,505
|
| 52 |
+
cryptography/hazmat/bindings/_rust/pkcs12.pyi,sha256=vEEd5wDiZvb8ZGFaziLCaWLzAwoG_tvPUxLQw5_uOl8,1605
|
| 53 |
+
cryptography/hazmat/bindings/_rust/pkcs7.pyi,sha256=txGBJijqZshEcqra6byPNbnisIdlxzOSIHP2hl9arPs,1601
|
| 54 |
+
cryptography/hazmat/bindings/_rust/test_support.pyi,sha256=PPhld-WkO743iXFPebeG0LtgK0aTzGdjcIsay1Gm5GE,757
|
| 55 |
+
cryptography/hazmat/bindings/_rust/x509.pyi,sha256=WWFr7RV3n1eFYbLpmM_xwXGKYZalyYi_V07p6KLOHX8,10189
|
| 56 |
+
cryptography/hazmat/bindings/openssl/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
| 57 |
+
cryptography/hazmat/bindings/openssl/__pycache__/__init__.cpython-310.pyc,,
|
| 58 |
+
cryptography/hazmat/bindings/openssl/__pycache__/_conditional.cpython-310.pyc,,
|
| 59 |
+
cryptography/hazmat/bindings/openssl/__pycache__/binding.cpython-310.pyc,,
|
| 60 |
+
cryptography/hazmat/bindings/openssl/_conditional.py,sha256=eKiDLdRSXTGA6mQ8jj5S2OXEB_sod6iL7cwed-yOuUw,5340
|
| 61 |
+
cryptography/hazmat/bindings/openssl/binding.py,sha256=hAmRXYzQlpIAP_jj2tl-SIBJBxwtclLlr_3aIL-RkR0,4072
|
| 62 |
+
cryptography/hazmat/decrepit/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
|
| 63 |
+
cryptography/hazmat/decrepit/__pycache__/__init__.cpython-310.pyc,,
|
| 64 |
+
cryptography/hazmat/decrepit/ciphers/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
|
| 65 |
+
cryptography/hazmat/decrepit/ciphers/__pycache__/__init__.cpython-310.pyc,,
|
| 66 |
+
cryptography/hazmat/decrepit/ciphers/__pycache__/algorithms.cpython-310.pyc,,
|
| 67 |
+
cryptography/hazmat/decrepit/ciphers/algorithms.py,sha256=YrKgHS4MfwWaMmPBYRymRRlC0phwWp9ycICFezeJPGk,2595
|
| 68 |
+
cryptography/hazmat/primitives/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
| 69 |
+
cryptography/hazmat/primitives/__pycache__/__init__.cpython-310.pyc,,
|
| 70 |
+
cryptography/hazmat/primitives/__pycache__/_asymmetric.cpython-310.pyc,,
|
| 71 |
+
cryptography/hazmat/primitives/__pycache__/_cipheralgorithm.cpython-310.pyc,,
|
| 72 |
+
cryptography/hazmat/primitives/__pycache__/_serialization.cpython-310.pyc,,
|
| 73 |
+
cryptography/hazmat/primitives/__pycache__/cmac.cpython-310.pyc,,
|
| 74 |
+
cryptography/hazmat/primitives/__pycache__/constant_time.cpython-310.pyc,,
|
| 75 |
+
cryptography/hazmat/primitives/__pycache__/hashes.cpython-310.pyc,,
|
| 76 |
+
cryptography/hazmat/primitives/__pycache__/hmac.cpython-310.pyc,,
|
| 77 |
+
cryptography/hazmat/primitives/__pycache__/keywrap.cpython-310.pyc,,
|
| 78 |
+
cryptography/hazmat/primitives/__pycache__/padding.cpython-310.pyc,,
|
| 79 |
+
cryptography/hazmat/primitives/__pycache__/poly1305.cpython-310.pyc,,
|
| 80 |
+
cryptography/hazmat/primitives/_asymmetric.py,sha256=RhgcouUB6HTiFDBrR1LxqkMjpUxIiNvQ1r_zJjRG6qQ,532
|
| 81 |
+
cryptography/hazmat/primitives/_cipheralgorithm.py,sha256=Eh3i7lwedHfi0eLSsH93PZxQKzY9I6lkK67vL4V5tOc,1522
|
| 82 |
+
cryptography/hazmat/primitives/_serialization.py,sha256=chgPCSF2jxI2Cr5gB-qbWXOvOfupBh4CARS0KAhv9AM,5123
|
| 83 |
+
cryptography/hazmat/primitives/asymmetric/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
| 84 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/__init__.cpython-310.pyc,,
|
| 85 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/dh.cpython-310.pyc,,
|
| 86 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/dsa.cpython-310.pyc,,
|
| 87 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/ec.cpython-310.pyc,,
|
| 88 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/ed25519.cpython-310.pyc,,
|
| 89 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/ed448.cpython-310.pyc,,
|
| 90 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/padding.cpython-310.pyc,,
|
| 91 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/rsa.cpython-310.pyc,,
|
| 92 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/types.cpython-310.pyc,,
|
| 93 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/utils.cpython-310.pyc,,
|
| 94 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/x25519.cpython-310.pyc,,
|
| 95 |
+
cryptography/hazmat/primitives/asymmetric/__pycache__/x448.cpython-310.pyc,,
|
| 96 |
+
cryptography/hazmat/primitives/asymmetric/dh.py,sha256=0v_vEFFz5pQ1QG-FkWDyvgv7IfuVZSH5Q6LyFI5A8rg,3645
|
| 97 |
+
cryptography/hazmat/primitives/asymmetric/dsa.py,sha256=Ld_bbbqQFz12dObHxIkzEQzX0SWWP41RLSWkYSaKhqE,4213
|
| 98 |
+
cryptography/hazmat/primitives/asymmetric/ec.py,sha256=Vf5ig2PcS3PVnsb5N49Kx1uIkFBJyhg4BWXThDz5cug,12999
|
| 99 |
+
cryptography/hazmat/primitives/asymmetric/ed25519.py,sha256=jZW5cs472wXXV3eB0sE1b8w64gdazwwU0_MT5UOTiXs,3700
|
| 100 |
+
cryptography/hazmat/primitives/asymmetric/ed448.py,sha256=yAetgn2f2JYf0BO8MapGzXeThsvSMG5LmUCrxVOidAA,3729
|
| 101 |
+
cryptography/hazmat/primitives/asymmetric/padding.py,sha256=eZcvUqVLbe3u48SunLdeniaPlV4-k6pwBl67OW4jSy8,2885
|
| 102 |
+
cryptography/hazmat/primitives/asymmetric/rsa.py,sha256=kegiZAGeb6yJISjpxpaAEpM3wGdfhJSU9RgZpVwKYwk,7967
|
| 103 |
+
cryptography/hazmat/primitives/asymmetric/types.py,sha256=LnsOJym-wmPUJ7Knu_7bCNU3kIiELCd6krOaW_JU08I,2996
|
| 104 |
+
cryptography/hazmat/primitives/asymmetric/utils.py,sha256=DPTs6T4F-UhwzFQTh-1fSEpQzazH2jf2xpIro3ItF4o,790
|
| 105 |
+
cryptography/hazmat/primitives/asymmetric/x25519.py,sha256=_4nQeZ3yJ3Lg0RpXnaqA-1yt6vbx1F-wzLcaZHwSpeE,3613
|
| 106 |
+
cryptography/hazmat/primitives/asymmetric/x448.py,sha256=WKBLtuVfJqiBRro654fGaQAlvsKbqbNkK7c4A_ZCdV0,3642
|
| 107 |
+
cryptography/hazmat/primitives/ciphers/__init__.py,sha256=eyEXmjk6_CZXaOPYDr7vAYGXr29QvzgWL2-4CSolLFs,680
|
| 108 |
+
cryptography/hazmat/primitives/ciphers/__pycache__/__init__.cpython-310.pyc,,
|
| 109 |
+
cryptography/hazmat/primitives/ciphers/__pycache__/aead.cpython-310.pyc,,
|
| 110 |
+
cryptography/hazmat/primitives/ciphers/__pycache__/algorithms.cpython-310.pyc,,
|
| 111 |
+
cryptography/hazmat/primitives/ciphers/__pycache__/base.cpython-310.pyc,,
|
| 112 |
+
cryptography/hazmat/primitives/ciphers/__pycache__/modes.cpython-310.pyc,,
|
| 113 |
+
cryptography/hazmat/primitives/ciphers/aead.py,sha256=Fzlyx7w8KYQakzDp1zWgJnIr62zgZrgVh1u2h4exB54,634
|
| 114 |
+
cryptography/hazmat/primitives/ciphers/algorithms.py,sha256=S04j4NdFCBDt5KLIvbYohohRO--MCWhTia_lkVt8xD8,4542
|
| 115 |
+
cryptography/hazmat/primitives/ciphers/base.py,sha256=aBC7HHBBoixebmparVr0UlODs3VD0A7B6oz_AaRjDv8,4253
|
| 116 |
+
cryptography/hazmat/primitives/ciphers/modes.py,sha256=20stpwhDtbAvpH0SMf9EDHIciwmTF-JMBUOZ9bU8WiQ,8318
|
| 117 |
+
cryptography/hazmat/primitives/cmac.py,sha256=sz_s6H_cYnOvx-VNWdIKhRhe3Ymp8z8J0D3CBqOX3gg,338
|
| 118 |
+
cryptography/hazmat/primitives/constant_time.py,sha256=xdunWT0nf8OvKdcqUhhlFKayGp4_PgVJRU2W1wLSr_A,422
|
| 119 |
+
cryptography/hazmat/primitives/hashes.py,sha256=M8BrlKB3U6DEtHvWTV5VRjpteHv1kS3Zxm_Bsk04cr8,5184
|
| 120 |
+
cryptography/hazmat/primitives/hmac.py,sha256=RpB3z9z5skirCQrm7zQbtnp9pLMnAjrlTUvKqF5aDDc,423
|
| 121 |
+
cryptography/hazmat/primitives/kdf/__init__.py,sha256=4XibZnrYq4hh5xBjWiIXzaYW6FKx8hPbVaa_cB9zS64,750
|
| 122 |
+
cryptography/hazmat/primitives/kdf/__pycache__/__init__.cpython-310.pyc,,
|
| 123 |
+
cryptography/hazmat/primitives/kdf/__pycache__/argon2.cpython-310.pyc,,
|
| 124 |
+
cryptography/hazmat/primitives/kdf/__pycache__/concatkdf.cpython-310.pyc,,
|
| 125 |
+
cryptography/hazmat/primitives/kdf/__pycache__/hkdf.cpython-310.pyc,,
|
| 126 |
+
cryptography/hazmat/primitives/kdf/__pycache__/kbkdf.cpython-310.pyc,,
|
| 127 |
+
cryptography/hazmat/primitives/kdf/__pycache__/pbkdf2.cpython-310.pyc,,
|
| 128 |
+
cryptography/hazmat/primitives/kdf/__pycache__/scrypt.cpython-310.pyc,,
|
| 129 |
+
cryptography/hazmat/primitives/kdf/__pycache__/x963kdf.cpython-310.pyc,,
|
| 130 |
+
cryptography/hazmat/primitives/kdf/argon2.py,sha256=UFDNXG0v-rw3DqAQTB1UQAsQC2M5Ejg0k_6OCyhLKus,460
|
| 131 |
+
cryptography/hazmat/primitives/kdf/concatkdf.py,sha256=Ua8KoLXXnzgsrAUmHpyKymaPt8aPRP0EHEaBz7QCQ9I,3737
|
| 132 |
+
cryptography/hazmat/primitives/kdf/hkdf.py,sha256=2HlP_huUzGAy_FZWOn8L9qP9B5ke7XKmbiER7_2l__Q,3043
|
| 133 |
+
cryptography/hazmat/primitives/kdf/kbkdf.py,sha256=tGH5dQGEVaKYMEbgeqlaSPpqhlDMoF0xwCxk2vuhcTc,9211
|
| 134 |
+
cryptography/hazmat/primitives/kdf/pbkdf2.py,sha256=1WIwhELR0w8ztTpTu8BrFiYWmK3hUfJq08I79TxwieE,1957
|
| 135 |
+
cryptography/hazmat/primitives/kdf/scrypt.py,sha256=XyWUdUUmhuI9V6TqAPOvujCSMGv1XQdg0a21IWCmO-U,590
|
| 136 |
+
cryptography/hazmat/primitives/kdf/x963kdf.py,sha256=zLTcF665QFvXX2f8TS7fmBZTteXpFjKahzfjjQcCJyw,1999
|
| 137 |
+
cryptography/hazmat/primitives/keywrap.py,sha256=XV4Pj2fqSeD-RqZVvY2cA3j5_7RwJSFygYuLfk2ujCo,5650
|
| 138 |
+
cryptography/hazmat/primitives/padding.py,sha256=QT-U-NvV2eQGO1wVPbDiNGNSc9keRDS-ig5cQOrLz0E,1865
|
| 139 |
+
cryptography/hazmat/primitives/poly1305.py,sha256=P5EPQV-RB_FJPahpg01u0Ts4S_PnAmsroxIGXbGeRRo,355
|
| 140 |
+
cryptography/hazmat/primitives/serialization/__init__.py,sha256=Q7uTgDlt7n3WfsMT6jYwutC6DIg_7SEeoAm1GHZ5B5E,1705
|
| 141 |
+
cryptography/hazmat/primitives/serialization/__pycache__/__init__.cpython-310.pyc,,
|
| 142 |
+
cryptography/hazmat/primitives/serialization/__pycache__/base.cpython-310.pyc,,
|
| 143 |
+
cryptography/hazmat/primitives/serialization/__pycache__/pkcs12.cpython-310.pyc,,
|
| 144 |
+
cryptography/hazmat/primitives/serialization/__pycache__/pkcs7.cpython-310.pyc,,
|
| 145 |
+
cryptography/hazmat/primitives/serialization/__pycache__/ssh.cpython-310.pyc,,
|
| 146 |
+
cryptography/hazmat/primitives/serialization/base.py,sha256=ikq5MJIwp_oUnjiaBco_PmQwOTYuGi-XkYUYHKy8Vo0,615
|
| 147 |
+
cryptography/hazmat/primitives/serialization/pkcs12.py,sha256=mS9cFNG4afzvseoc5e1MWoY2VskfL8N8Y_OFjl67luY,5104
|
| 148 |
+
cryptography/hazmat/primitives/serialization/pkcs7.py,sha256=5OR_Tkysxaprn4FegvJIfbep9rJ9wok6FLWvWwQ5-Mg,13943
|
| 149 |
+
cryptography/hazmat/primitives/serialization/ssh.py,sha256=CeDSD_KmtcpQDSUg8QY3jruZDOKjjWBKcUByDOGQXnY,53693
|
| 150 |
+
cryptography/hazmat/primitives/twofactor/__init__.py,sha256=tmMZGB-g4IU1r7lIFqASU019zr0uPp_wEBYcwdDCKCA,258
|
| 151 |
+
cryptography/hazmat/primitives/twofactor/__pycache__/__init__.cpython-310.pyc,,
|
| 152 |
+
cryptography/hazmat/primitives/twofactor/__pycache__/hotp.cpython-310.pyc,,
|
| 153 |
+
cryptography/hazmat/primitives/twofactor/__pycache__/totp.cpython-310.pyc,,
|
| 154 |
+
cryptography/hazmat/primitives/twofactor/hotp.py,sha256=ivZo5BrcCGWLsqql4nZV0XXCjyGPi_iHfDFltGlOJwk,3256
|
| 155 |
+
cryptography/hazmat/primitives/twofactor/totp.py,sha256=m5LPpRL00kp4zY8gTjr55Hfz9aMlPS53kHmVkSQCmdY,1652
|
| 156 |
+
cryptography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 157 |
+
cryptography/utils.py,sha256=r90VtamCjiba37omFqp2aa--eEQVXiQIMrvWeLT-BY4,4397
|
| 158 |
+
cryptography/x509/__init__.py,sha256=xloN0swseNx-m2WFZmCA17gOoxQWqeU82UVjEdJBePQ,8257
|
| 159 |
+
cryptography/x509/__pycache__/__init__.cpython-310.pyc,,
|
| 160 |
+
cryptography/x509/__pycache__/base.cpython-310.pyc,,
|
| 161 |
+
cryptography/x509/__pycache__/certificate_transparency.cpython-310.pyc,,
|
| 162 |
+
cryptography/x509/__pycache__/extensions.cpython-310.pyc,,
|
| 163 |
+
cryptography/x509/__pycache__/general_name.cpython-310.pyc,,
|
| 164 |
+
cryptography/x509/__pycache__/name.cpython-310.pyc,,
|
| 165 |
+
cryptography/x509/__pycache__/ocsp.cpython-310.pyc,,
|
| 166 |
+
cryptography/x509/__pycache__/oid.cpython-310.pyc,,
|
| 167 |
+
cryptography/x509/__pycache__/verification.cpython-310.pyc,,
|
| 168 |
+
cryptography/x509/base.py,sha256=OrmTw3y8B6AE_nGXQPN8x9kq-d7rDWeH13gCq6T6D6U,27997
|
| 169 |
+
cryptography/x509/certificate_transparency.py,sha256=JqoOIDhlwInrYMFW6IFn77WJ0viF-PB_rlZV3vs9MYc,797
|
| 170 |
+
cryptography/x509/extensions.py,sha256=QxYrqR6SF1qzR9ZraP8wDiIczlEVlAFuwDRVcltB6Tk,77724
|
| 171 |
+
cryptography/x509/general_name.py,sha256=sP_rV11Qlpsk4x3XXGJY_Mv0Q_s9dtjeLckHsjpLQoQ,7836
|
| 172 |
+
cryptography/x509/name.py,sha256=oi0CqY_B72CEJgNXy9XkJya26QWXfwOeGJBYfXjRYvI,15121
|
| 173 |
+
cryptography/x509/ocsp.py,sha256=Yey6NdFV1MPjop24Mj_VenjEpg3kUaMopSWOK0AbeBs,12699
|
| 174 |
+
cryptography/x509/oid.py,sha256=BUzgXXGVWilkBkdKPTm9R4qElE9gAGHgdYPMZAp7PJo,931
|
| 175 |
+
cryptography/x509/verification.py,sha256=gR2C2c-XZQtblZhT5T5vjSKOtCb74ef2alPVmEcwFlM,958
|
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: maturin (1.8.6)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp37-abi3-manylinux_2_34_x86_64
|
| 5 |
+
|
venv/lib/python3.10/site-packages/cryptography-45.0.5.dist-info/licenses/LICENSE.APACHE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
https://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
https://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
venv/lib/python3.10/site-packages/git/__init__.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
# @PydevCodeAnalysisIgnore
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"Actor",
|
| 10 |
+
"AmbiguousObjectName",
|
| 11 |
+
"BadName",
|
| 12 |
+
"BadObject",
|
| 13 |
+
"BadObjectType",
|
| 14 |
+
"BaseIndexEntry",
|
| 15 |
+
"Blob",
|
| 16 |
+
"BlobFilter",
|
| 17 |
+
"BlockingLockFile",
|
| 18 |
+
"CacheError",
|
| 19 |
+
"CheckoutError",
|
| 20 |
+
"CommandError",
|
| 21 |
+
"Commit",
|
| 22 |
+
"Diff",
|
| 23 |
+
"DiffConstants",
|
| 24 |
+
"DiffIndex",
|
| 25 |
+
"Diffable",
|
| 26 |
+
"FetchInfo",
|
| 27 |
+
"Git",
|
| 28 |
+
"GitCmdObjectDB",
|
| 29 |
+
"GitCommandError",
|
| 30 |
+
"GitCommandNotFound",
|
| 31 |
+
"GitConfigParser",
|
| 32 |
+
"GitDB",
|
| 33 |
+
"GitError",
|
| 34 |
+
"HEAD",
|
| 35 |
+
"Head",
|
| 36 |
+
"HookExecutionError",
|
| 37 |
+
"INDEX",
|
| 38 |
+
"IndexEntry",
|
| 39 |
+
"IndexFile",
|
| 40 |
+
"IndexObject",
|
| 41 |
+
"InvalidDBRoot",
|
| 42 |
+
"InvalidGitRepositoryError",
|
| 43 |
+
"List", # Deprecated - import this from `typing` instead.
|
| 44 |
+
"LockFile",
|
| 45 |
+
"NULL_TREE",
|
| 46 |
+
"NoSuchPathError",
|
| 47 |
+
"ODBError",
|
| 48 |
+
"Object",
|
| 49 |
+
"Optional", # Deprecated - import this from `typing` instead.
|
| 50 |
+
"ParseError",
|
| 51 |
+
"PathLike",
|
| 52 |
+
"PushInfo",
|
| 53 |
+
"RefLog",
|
| 54 |
+
"RefLogEntry",
|
| 55 |
+
"Reference",
|
| 56 |
+
"Remote",
|
| 57 |
+
"RemoteProgress",
|
| 58 |
+
"RemoteReference",
|
| 59 |
+
"Repo",
|
| 60 |
+
"RepositoryDirtyError",
|
| 61 |
+
"RootModule",
|
| 62 |
+
"RootUpdateProgress",
|
| 63 |
+
"Sequence", # Deprecated - import from `typing`, or `collections.abc` in 3.9+.
|
| 64 |
+
"StageType",
|
| 65 |
+
"Stats",
|
| 66 |
+
"Submodule",
|
| 67 |
+
"SymbolicReference",
|
| 68 |
+
"TYPE_CHECKING", # Deprecated - import this from `typing` instead.
|
| 69 |
+
"Tag",
|
| 70 |
+
"TagObject",
|
| 71 |
+
"TagReference",
|
| 72 |
+
"Tree",
|
| 73 |
+
"TreeModifier",
|
| 74 |
+
"Tuple", # Deprecated - import this from `typing` instead.
|
| 75 |
+
"Union", # Deprecated - import this from `typing` instead.
|
| 76 |
+
"UnmergedEntriesError",
|
| 77 |
+
"UnsafeOptionError",
|
| 78 |
+
"UnsafeProtocolError",
|
| 79 |
+
"UnsupportedOperation",
|
| 80 |
+
"UpdateProgress",
|
| 81 |
+
"WorkTreeRepositoryUnsupported",
|
| 82 |
+
"refresh",
|
| 83 |
+
"remove_password_if_present",
|
| 84 |
+
"rmtree",
|
| 85 |
+
"safe_decode",
|
| 86 |
+
"to_hex_sha",
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
__version__ = '3.1.44'
|
| 90 |
+
|
| 91 |
+
from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
|
| 92 |
+
|
| 93 |
+
if TYPE_CHECKING:
|
| 94 |
+
from types import ModuleType
|
| 95 |
+
|
| 96 |
+
import warnings
|
| 97 |
+
|
| 98 |
+
from gitdb.util import to_hex_sha
|
| 99 |
+
|
| 100 |
+
from git.exc import (
|
| 101 |
+
AmbiguousObjectName,
|
| 102 |
+
BadName,
|
| 103 |
+
BadObject,
|
| 104 |
+
BadObjectType,
|
| 105 |
+
CacheError,
|
| 106 |
+
CheckoutError,
|
| 107 |
+
CommandError,
|
| 108 |
+
GitCommandError,
|
| 109 |
+
GitCommandNotFound,
|
| 110 |
+
GitError,
|
| 111 |
+
HookExecutionError,
|
| 112 |
+
InvalidDBRoot,
|
| 113 |
+
InvalidGitRepositoryError,
|
| 114 |
+
NoSuchPathError,
|
| 115 |
+
ODBError,
|
| 116 |
+
ParseError,
|
| 117 |
+
RepositoryDirtyError,
|
| 118 |
+
UnmergedEntriesError,
|
| 119 |
+
UnsafeOptionError,
|
| 120 |
+
UnsafeProtocolError,
|
| 121 |
+
UnsupportedOperation,
|
| 122 |
+
WorkTreeRepositoryUnsupported,
|
| 123 |
+
)
|
| 124 |
+
from git.types import PathLike
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
from git.compat import safe_decode # @NoMove
|
| 128 |
+
from git.config import GitConfigParser # @NoMove
|
| 129 |
+
from git.objects import ( # @NoMove
|
| 130 |
+
Blob,
|
| 131 |
+
Commit,
|
| 132 |
+
IndexObject,
|
| 133 |
+
Object,
|
| 134 |
+
RootModule,
|
| 135 |
+
RootUpdateProgress,
|
| 136 |
+
Submodule,
|
| 137 |
+
TagObject,
|
| 138 |
+
Tree,
|
| 139 |
+
TreeModifier,
|
| 140 |
+
UpdateProgress,
|
| 141 |
+
)
|
| 142 |
+
from git.refs import ( # @NoMove
|
| 143 |
+
HEAD,
|
| 144 |
+
Head,
|
| 145 |
+
RefLog,
|
| 146 |
+
RefLogEntry,
|
| 147 |
+
Reference,
|
| 148 |
+
RemoteReference,
|
| 149 |
+
SymbolicReference,
|
| 150 |
+
Tag,
|
| 151 |
+
TagReference,
|
| 152 |
+
)
|
| 153 |
+
from git.diff import ( # @NoMove
|
| 154 |
+
INDEX,
|
| 155 |
+
NULL_TREE,
|
| 156 |
+
Diff,
|
| 157 |
+
DiffConstants,
|
| 158 |
+
DiffIndex,
|
| 159 |
+
Diffable,
|
| 160 |
+
)
|
| 161 |
+
from git.db import GitCmdObjectDB, GitDB # @NoMove
|
| 162 |
+
from git.cmd import Git # @NoMove
|
| 163 |
+
from git.repo import Repo # @NoMove
|
| 164 |
+
from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove
|
| 165 |
+
from git.index import ( # @NoMove
|
| 166 |
+
BaseIndexEntry,
|
| 167 |
+
BlobFilter,
|
| 168 |
+
CheckoutError,
|
| 169 |
+
IndexEntry,
|
| 170 |
+
IndexFile,
|
| 171 |
+
StageType,
|
| 172 |
+
# NOTE: This tells type checkers what util resolves to. We delete it, and it is
|
| 173 |
+
# really resolved by __getattr__, which warns. See below on what to use instead.
|
| 174 |
+
util,
|
| 175 |
+
)
|
| 176 |
+
from git.util import ( # @NoMove
|
| 177 |
+
Actor,
|
| 178 |
+
BlockingLockFile,
|
| 179 |
+
LockFile,
|
| 180 |
+
Stats,
|
| 181 |
+
remove_password_if_present,
|
| 182 |
+
rmtree,
|
| 183 |
+
)
|
| 184 |
+
except GitError as _exc:
|
| 185 |
+
raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _warned_import(message: str, fullname: str) -> "ModuleType":
|
| 189 |
+
import importlib
|
| 190 |
+
|
| 191 |
+
warnings.warn(message, DeprecationWarning, stacklevel=3)
|
| 192 |
+
return importlib.import_module(fullname)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _getattr(name: str) -> Any:
|
| 196 |
+
# TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
|
| 197 |
+
|
| 198 |
+
if name == "util":
|
| 199 |
+
return _warned_import(
|
| 200 |
+
"The expression `git.util` and the import `from git import util` actually "
|
| 201 |
+
"reference git.index.util, and not the git.util module accessed in "
|
| 202 |
+
'`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
|
| 203 |
+
"confusing behavior is currently preserved for compatibility, but may be "
|
| 204 |
+
"changed in the future and should not be relied on.",
|
| 205 |
+
fullname="git.index.util",
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
for names, prefix in (
|
| 209 |
+
({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
|
| 210 |
+
({"base", "fun", "typ"}, "git.index"),
|
| 211 |
+
):
|
| 212 |
+
if name not in names:
|
| 213 |
+
continue
|
| 214 |
+
|
| 215 |
+
fullname = f"{prefix}.{name}"
|
| 216 |
+
|
| 217 |
+
return _warned_import(
|
| 218 |
+
f"{__name__}.{name} is a private alias of {fullname} and subject to "
|
| 219 |
+
f"immediate removal. Use {fullname} instead.",
|
| 220 |
+
fullname=fullname,
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
if not TYPE_CHECKING:
|
| 227 |
+
# NOTE: The expression `git.util` gives git.index.util and `from git import util`
|
| 228 |
+
# imports git.index.util, NOT git.util. It may not be feasible to change this until
|
| 229 |
+
# the next major version, to avoid breaking code inadvertently relying on it.
|
| 230 |
+
#
|
| 231 |
+
# - If git.index.util *is* what you want, use (or import from) that, to avoid
|
| 232 |
+
# confusion.
|
| 233 |
+
#
|
| 234 |
+
# - To use the "real" git.util module, write `from git.util import ...`, or if
|
| 235 |
+
# necessary access it as `sys.modules["git.util"]`.
|
| 236 |
+
#
|
| 237 |
+
# Note also that `import git.util` technically imports the "real" git.util... but
|
| 238 |
+
# the *expression* `git.util` after doing so is still git.index.util!
|
| 239 |
+
#
|
| 240 |
+
# (This situation differs from that of other indirect-submodule imports that are
|
| 241 |
+
# unambiguously non-public and subject to immediate removal. Here, the public
|
| 242 |
+
# git.util module, though different, makes less discoverable that the expression
|
| 243 |
+
# `git.util` refers to a non-public attribute of the git module.)
|
| 244 |
+
#
|
| 245 |
+
# This had originally come about by a wildcard import. Now that all intended imports
|
| 246 |
+
# are explicit, the intuitive but potentially incompatible binding occurs due to the
|
| 247 |
+
# usual rules for Python submodule bindings. So for now we replace that binding with
|
| 248 |
+
# git.index.util, delete that, and let __getattr__ handle it and issue a warning.
|
| 249 |
+
#
|
| 250 |
+
# For the same runtime behavior, it would be enough to forgo importing util, and
|
| 251 |
+
# delete util as created naturally; __getattr__ would behave the same. But type
|
| 252 |
+
# checkers would not know what util refers to when accessed as an attribute of git.
|
| 253 |
+
del util
|
| 254 |
+
|
| 255 |
+
# This is "hidden" to preserve static checking for undefined/misspelled attributes.
|
| 256 |
+
__getattr__ = _getattr
|
| 257 |
+
|
| 258 |
+
# { Initialize git executable path
|
| 259 |
+
|
| 260 |
+
GIT_OK = None
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def refresh(path: Optional[PathLike] = None) -> None:
|
| 264 |
+
"""Convenience method for setting the git executable path.
|
| 265 |
+
|
| 266 |
+
:param path:
|
| 267 |
+
Optional path to the Git executable. If not absolute, it is resolved
|
| 268 |
+
immediately, relative to the current directory.
|
| 269 |
+
|
| 270 |
+
:note:
|
| 271 |
+
The `path` parameter is usually omitted and cannot be used to specify a custom
|
| 272 |
+
command whose location is looked up in a path search on each call. See
|
| 273 |
+
:meth:`Git.refresh <git.cmd.Git.refresh>` for details on how to achieve this.
|
| 274 |
+
|
| 275 |
+
:note:
|
| 276 |
+
This calls :meth:`Git.refresh <git.cmd.Git.refresh>` and sets other global
|
| 277 |
+
configuration according to the effect of doing so. As such, this function should
|
| 278 |
+
usually be used instead of using :meth:`Git.refresh <git.cmd.Git.refresh>` or
|
| 279 |
+
:meth:`FetchInfo.refresh <git.remote.FetchInfo.refresh>` directly.
|
| 280 |
+
|
| 281 |
+
:note:
|
| 282 |
+
This function is called automatically, with no arguments, at import time.
|
| 283 |
+
"""
|
| 284 |
+
global GIT_OK
|
| 285 |
+
GIT_OK = False
|
| 286 |
+
|
| 287 |
+
if not Git.refresh(path=path):
|
| 288 |
+
return
|
| 289 |
+
if not FetchInfo.refresh(): # noqa: F405
|
| 290 |
+
return # type: ignore[unreachable]
|
| 291 |
+
|
| 292 |
+
GIT_OK = True
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
try:
|
| 296 |
+
refresh()
|
| 297 |
+
except Exception as _exc:
|
| 298 |
+
raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc
|
| 299 |
+
|
| 300 |
+
# } END initialize git executable path
|
venv/lib/python3.10/site-packages/git/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (5.29 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/cmd.cpython-310.pyc
ADDED
|
Binary file (47.9 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/compat.cpython-310.pyc
ADDED
|
Binary file (3.27 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/config.cpython-310.pyc
ADDED
|
Binary file (27.4 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/db.cpython-310.pyc
ADDED
|
Binary file (2.61 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/diff.cpython-310.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/exc.cpython-310.pyc
ADDED
|
Binary file (7.72 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/remote.cpython-310.pyc
ADDED
|
Binary file (35.1 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/types.cpython-310.pyc
ADDED
|
Binary file (4.83 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/__pycache__/util.cpython-310.pyc
ADDED
|
Binary file (39.3 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/cmd.py
ADDED
|
@@ -0,0 +1,1724 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
__all__ = ["GitMeta", "Git"]
|
| 9 |
+
|
| 10 |
+
import contextlib
|
| 11 |
+
import io
|
| 12 |
+
import itertools
|
| 13 |
+
import logging
|
| 14 |
+
import os
|
| 15 |
+
import re
|
| 16 |
+
import signal
|
| 17 |
+
import subprocess
|
| 18 |
+
from subprocess import DEVNULL, PIPE, Popen
|
| 19 |
+
import sys
|
| 20 |
+
from textwrap import dedent
|
| 21 |
+
import threading
|
| 22 |
+
import warnings
|
| 23 |
+
|
| 24 |
+
from git.compat import defenc, force_bytes, safe_decode
|
| 25 |
+
from git.exc import (
|
| 26 |
+
CommandError,
|
| 27 |
+
GitCommandError,
|
| 28 |
+
GitCommandNotFound,
|
| 29 |
+
UnsafeOptionError,
|
| 30 |
+
UnsafeProtocolError,
|
| 31 |
+
)
|
| 32 |
+
from git.util import (
|
| 33 |
+
cygpath,
|
| 34 |
+
expand_path,
|
| 35 |
+
is_cygwin_git,
|
| 36 |
+
patch_env,
|
| 37 |
+
remove_password_if_present,
|
| 38 |
+
stream_copy,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# typing ---------------------------------------------------------------------------
|
| 42 |
+
|
| 43 |
+
from typing import (
|
| 44 |
+
Any,
|
| 45 |
+
AnyStr,
|
| 46 |
+
BinaryIO,
|
| 47 |
+
Callable,
|
| 48 |
+
Dict,
|
| 49 |
+
IO,
|
| 50 |
+
Iterator,
|
| 51 |
+
List,
|
| 52 |
+
Mapping,
|
| 53 |
+
Optional,
|
| 54 |
+
Sequence,
|
| 55 |
+
TYPE_CHECKING,
|
| 56 |
+
TextIO,
|
| 57 |
+
Tuple,
|
| 58 |
+
Union,
|
| 59 |
+
cast,
|
| 60 |
+
overload,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
from git.types import Literal, PathLike, TBD
|
| 64 |
+
|
| 65 |
+
if TYPE_CHECKING:
|
| 66 |
+
from git.diff import DiffIndex
|
| 67 |
+
from git.repo.base import Repo
|
| 68 |
+
|
| 69 |
+
# ---------------------------------------------------------------------------------
|
| 70 |
+
|
| 71 |
+
execute_kwargs = {
|
| 72 |
+
"istream",
|
| 73 |
+
"with_extended_output",
|
| 74 |
+
"with_exceptions",
|
| 75 |
+
"as_process",
|
| 76 |
+
"output_stream",
|
| 77 |
+
"stdout_as_string",
|
| 78 |
+
"kill_after_timeout",
|
| 79 |
+
"with_stdout",
|
| 80 |
+
"universal_newlines",
|
| 81 |
+
"shell",
|
| 82 |
+
"env",
|
| 83 |
+
"max_chunk_size",
|
| 84 |
+
"strip_newline_in_stdout",
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
_logger = logging.getLogger(__name__)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ==============================================================================
|
| 91 |
+
## @name Utilities
|
| 92 |
+
# ------------------------------------------------------------------------------
|
| 93 |
+
# Documentation
|
| 94 |
+
## @{
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def handle_process_output(
|
| 98 |
+
process: "Git.AutoInterrupt" | Popen,
|
| 99 |
+
stdout_handler: Union[
|
| 100 |
+
None,
|
| 101 |
+
Callable[[AnyStr], None],
|
| 102 |
+
Callable[[List[AnyStr]], None],
|
| 103 |
+
Callable[[bytes, "Repo", "DiffIndex"], None],
|
| 104 |
+
],
|
| 105 |
+
stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
|
| 106 |
+
finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
|
| 107 |
+
decode_streams: bool = True,
|
| 108 |
+
kill_after_timeout: Union[None, float] = None,
|
| 109 |
+
) -> None:
|
| 110 |
+
R"""Register for notifications to learn that process output is ready to read, and
|
| 111 |
+
dispatch lines to the respective line handlers.
|
| 112 |
+
|
| 113 |
+
This function returns once the finalizer returns.
|
| 114 |
+
|
| 115 |
+
:param process:
|
| 116 |
+
:class:`subprocess.Popen` instance.
|
| 117 |
+
|
| 118 |
+
:param stdout_handler:
|
| 119 |
+
f(stdout_line_string), or ``None``.
|
| 120 |
+
|
| 121 |
+
:param stderr_handler:
|
| 122 |
+
f(stderr_line_string), or ``None``.
|
| 123 |
+
|
| 124 |
+
:param finalizer:
|
| 125 |
+
f(proc) - wait for proc to finish.
|
| 126 |
+
|
| 127 |
+
:param decode_streams:
|
| 128 |
+
Assume stdout/stderr streams are binary and decode them before pushing their
|
| 129 |
+
contents to handlers.
|
| 130 |
+
|
| 131 |
+
This defaults to ``True``. Set it to ``False`` if:
|
| 132 |
+
|
| 133 |
+
- ``universal_newlines == True``, as then streams are in text mode, or
|
| 134 |
+
- decoding must happen later, such as for :class:`~git.diff.Diff`\s.
|
| 135 |
+
|
| 136 |
+
:param kill_after_timeout:
|
| 137 |
+
:class:`float` or ``None``, Default = ``None``
|
| 138 |
+
|
| 139 |
+
To specify a timeout in seconds for the git command, after which the process
|
| 140 |
+
should be killed.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
# Use 2 "pump" threads and wait for both to finish.
|
| 144 |
+
def pump_stream(
|
| 145 |
+
cmdline: List[str],
|
| 146 |
+
name: str,
|
| 147 |
+
stream: Union[BinaryIO, TextIO],
|
| 148 |
+
is_decode: bool,
|
| 149 |
+
handler: Union[None, Callable[[Union[bytes, str]], None]],
|
| 150 |
+
) -> None:
|
| 151 |
+
try:
|
| 152 |
+
for line in stream:
|
| 153 |
+
if handler:
|
| 154 |
+
if is_decode:
|
| 155 |
+
assert isinstance(line, bytes)
|
| 156 |
+
line_str = line.decode(defenc)
|
| 157 |
+
handler(line_str)
|
| 158 |
+
else:
|
| 159 |
+
handler(line)
|
| 160 |
+
|
| 161 |
+
except Exception as ex:
|
| 162 |
+
_logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
|
| 163 |
+
if "I/O operation on closed file" not in str(ex):
|
| 164 |
+
# Only reraise if the error was not due to the stream closing.
|
| 165 |
+
raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
|
| 166 |
+
finally:
|
| 167 |
+
stream.close()
|
| 168 |
+
|
| 169 |
+
if hasattr(process, "proc"):
|
| 170 |
+
process = cast("Git.AutoInterrupt", process)
|
| 171 |
+
cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "")
|
| 172 |
+
p_stdout = process.proc.stdout if process.proc else None
|
| 173 |
+
p_stderr = process.proc.stderr if process.proc else None
|
| 174 |
+
else:
|
| 175 |
+
process = cast(Popen, process) # type: ignore[redundant-cast]
|
| 176 |
+
cmdline = getattr(process, "args", "")
|
| 177 |
+
p_stdout = process.stdout
|
| 178 |
+
p_stderr = process.stderr
|
| 179 |
+
|
| 180 |
+
if not isinstance(cmdline, (tuple, list)):
|
| 181 |
+
cmdline = cmdline.split()
|
| 182 |
+
|
| 183 |
+
pumps: List[Tuple[str, IO, Callable[..., None] | None]] = []
|
| 184 |
+
if p_stdout:
|
| 185 |
+
pumps.append(("stdout", p_stdout, stdout_handler))
|
| 186 |
+
if p_stderr:
|
| 187 |
+
pumps.append(("stderr", p_stderr, stderr_handler))
|
| 188 |
+
|
| 189 |
+
threads: List[threading.Thread] = []
|
| 190 |
+
|
| 191 |
+
for name, stream, handler in pumps:
|
| 192 |
+
t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler))
|
| 193 |
+
t.daemon = True
|
| 194 |
+
t.start()
|
| 195 |
+
threads.append(t)
|
| 196 |
+
|
| 197 |
+
# FIXME: Why join? Will block if stdin needs feeding...
|
| 198 |
+
for t in threads:
|
| 199 |
+
t.join(timeout=kill_after_timeout)
|
| 200 |
+
if t.is_alive():
|
| 201 |
+
if isinstance(process, Git.AutoInterrupt):
|
| 202 |
+
process._terminate()
|
| 203 |
+
else: # Don't want to deal with the other case.
|
| 204 |
+
raise RuntimeError(
|
| 205 |
+
"Thread join() timed out in cmd.handle_process_output()."
|
| 206 |
+
f" kill_after_timeout={kill_after_timeout} seconds"
|
| 207 |
+
)
|
| 208 |
+
if stderr_handler:
|
| 209 |
+
error_str: Union[str, bytes] = (
|
| 210 |
+
"error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds"
|
| 211 |
+
)
|
| 212 |
+
if not decode_streams and isinstance(p_stderr, BinaryIO):
|
| 213 |
+
# Assume stderr_handler needs binary input.
|
| 214 |
+
error_str = cast(str, error_str)
|
| 215 |
+
error_str = error_str.encode()
|
| 216 |
+
# We ignore typing on the next line because mypy does not like the way
|
| 217 |
+
# we inferred that stderr takes str or bytes.
|
| 218 |
+
stderr_handler(error_str) # type: ignore[arg-type]
|
| 219 |
+
|
| 220 |
+
if finalizer:
|
| 221 |
+
finalizer(process)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
safer_popen: Callable[..., Popen]
|
| 225 |
+
|
| 226 |
+
if sys.platform == "win32":
|
| 227 |
+
|
| 228 |
+
def _safer_popen_windows(
|
| 229 |
+
command: Union[str, Sequence[Any]],
|
| 230 |
+
*,
|
| 231 |
+
shell: bool = False,
|
| 232 |
+
env: Optional[Mapping[str, str]] = None,
|
| 233 |
+
**kwargs: Any,
|
| 234 |
+
) -> Popen:
|
| 235 |
+
"""Call :class:`subprocess.Popen` on Windows but don't include a CWD in the
|
| 236 |
+
search.
|
| 237 |
+
|
| 238 |
+
This avoids an untrusted search path condition where a file like ``git.exe`` in
|
| 239 |
+
a malicious repository would be run when GitPython operates on the repository.
|
| 240 |
+
The process using GitPython may have an untrusted repository's working tree as
|
| 241 |
+
its current working directory. Some operations may temporarily change to that
|
| 242 |
+
directory before running a subprocess. In addition, while by default GitPython
|
| 243 |
+
does not run external commands with a shell, it can be made to do so, in which
|
| 244 |
+
case the CWD of the subprocess, which GitPython usually sets to a repository
|
| 245 |
+
working tree, can itself be searched automatically by the shell. This wrapper
|
| 246 |
+
covers all those cases.
|
| 247 |
+
|
| 248 |
+
:note:
|
| 249 |
+
This currently works by setting the
|
| 250 |
+
:envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during
|
| 251 |
+
subprocess creation. It also takes care of passing Windows-specific process
|
| 252 |
+
creation flags, but that is unrelated to path search.
|
| 253 |
+
|
| 254 |
+
:note:
|
| 255 |
+
The current implementation contains a race condition on :attr:`os.environ`.
|
| 256 |
+
GitPython isn't thread-safe, but a program using it on one thread should
|
| 257 |
+
ideally be able to mutate :attr:`os.environ` on another, without
|
| 258 |
+
unpredictable results. See comments in:
|
| 259 |
+
https://github.com/gitpython-developers/GitPython/pull/1650
|
| 260 |
+
"""
|
| 261 |
+
# CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards.
|
| 262 |
+
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
|
| 263 |
+
# https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
|
| 264 |
+
creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
|
| 265 |
+
|
| 266 |
+
# When using a shell, the shell is the direct subprocess, so the variable must
|
| 267 |
+
# be set in its environment, to affect its search behavior.
|
| 268 |
+
if shell:
|
| 269 |
+
# The original may be immutable, or the caller may reuse it. Mutate a copy.
|
| 270 |
+
env = {} if env is None else dict(env)
|
| 271 |
+
env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be an value.
|
| 272 |
+
|
| 273 |
+
# When not using a shell, the current process does the search in a
|
| 274 |
+
# CreateProcessW API call, so the variable must be set in our environment. With
|
| 275 |
+
# a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
|
| 276 |
+
# is patched. In Python versions where it is unpatched, and in the rare case the
|
| 277 |
+
# ComSpec environment variable is unset, the search for the shell itself is
|
| 278 |
+
# unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
|
| 279 |
+
# is simpler and protects against that. (As above, the "1" can be any value.)
|
| 280 |
+
with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
|
| 281 |
+
return Popen(
|
| 282 |
+
command,
|
| 283 |
+
shell=shell,
|
| 284 |
+
env=env,
|
| 285 |
+
creationflags=creationflags,
|
| 286 |
+
**kwargs,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
safer_popen = _safer_popen_windows
|
| 290 |
+
else:
|
| 291 |
+
safer_popen = Popen
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def dashify(string: str) -> str:
|
| 295 |
+
return string.replace("_", "-")
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]:
|
| 299 |
+
return {s: getattr(self, s) for s in self.__slots__ if s not in exclude}
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None:
|
| 303 |
+
for k, v in d.items():
|
| 304 |
+
setattr(self, k, v)
|
| 305 |
+
for k in excluded:
|
| 306 |
+
setattr(self, k, None)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
## -- End Utilities -- @}
|
| 310 |
+
|
| 311 |
+
_USE_SHELL_DEFAULT_MESSAGE = (
|
| 312 |
+
"Git.USE_SHELL is deprecated, because only its default value of False is safe. "
|
| 313 |
+
"It will be removed in a future release."
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
_USE_SHELL_DANGER_MESSAGE = (
|
| 317 |
+
"Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
|
| 318 |
+
"shell syntax cannot usually be accounted for. This can result in a command "
|
| 319 |
+
"injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
|
| 320 |
+
"and will be removed in a future release."
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def _warn_use_shell(extra_danger: bool) -> None:
|
| 325 |
+
warnings.warn(
|
| 326 |
+
_USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
|
| 327 |
+
DeprecationWarning,
|
| 328 |
+
stacklevel=3,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
class _GitMeta(type):
|
| 333 |
+
"""Metaclass for :class:`Git`.
|
| 334 |
+
|
| 335 |
+
This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
|
| 336 |
+
"""
|
| 337 |
+
|
| 338 |
+
def __getattribute(cls, name: str) -> Any:
|
| 339 |
+
if name == "USE_SHELL":
|
| 340 |
+
_warn_use_shell(False)
|
| 341 |
+
return super().__getattribute__(name)
|
| 342 |
+
|
| 343 |
+
def __setattr(cls, name: str, value: Any) -> Any:
|
| 344 |
+
if name == "USE_SHELL":
|
| 345 |
+
_warn_use_shell(value)
|
| 346 |
+
super().__setattr__(name, value)
|
| 347 |
+
|
| 348 |
+
if not TYPE_CHECKING:
|
| 349 |
+
# To preserve static checking for undefined/misspelled attributes while letting
|
| 350 |
+
# the methods' bodies be type-checked, these are defined as non-special methods,
|
| 351 |
+
# then bound to special names out of view of static type checkers. (The original
|
| 352 |
+
# names invoke name mangling (leading "__") to avoid confusion in other scopes.)
|
| 353 |
+
__getattribute__ = __getattribute
|
| 354 |
+
__setattr__ = __setattr
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
GitMeta = _GitMeta
|
| 358 |
+
"""Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
|
| 359 |
+
|
| 360 |
+
Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
|
| 361 |
+
custom metaclass is not documented and may change at any time. This statically checkable
|
| 362 |
+
metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
|
| 363 |
+
used. Code that benefits from it is likely to be remain brittle even if it is used.
|
| 364 |
+
|
| 365 |
+
In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
|
| 366 |
+
callable attributes representing git subcommands, it rarely makes sense to inherit from
|
| 367 |
+
:class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
|
| 368 |
+
to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
|
| 369 |
+
as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
|
| 370 |
+
might have to be introduced to solve a metaclass conflict, is not recommended.
|
| 371 |
+
|
| 372 |
+
:note:
|
| 373 |
+
The correct static type of the :class:`Git` class itself, and any subclasses, is
|
| 374 |
+
``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
|
| 375 |
+
|
| 376 |
+
:class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
|
| 377 |
+
intended or otherwise possible to use. This alias is truly only for very rare and
|
| 378 |
+
inherently precarious situations where it is necessary to deal with the metaclass
|
| 379 |
+
explicitly.
|
| 380 |
+
"""
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
class Git(metaclass=_GitMeta):
|
| 384 |
+
"""The Git class manages communication with the Git binary.
|
| 385 |
+
|
| 386 |
+
It provides a convenient interface to calling the Git binary, such as in::
|
| 387 |
+
|
| 388 |
+
g = Git( git_dir )
|
| 389 |
+
g.init() # calls 'git init' program
|
| 390 |
+
rval = g.ls_files() # calls 'git ls-files' program
|
| 391 |
+
|
| 392 |
+
Debugging:
|
| 393 |
+
|
| 394 |
+
* Set the :envvar:`GIT_PYTHON_TRACE` environment variable to print each invocation
|
| 395 |
+
of the command to stdout.
|
| 396 |
+
* Set its value to ``full`` to see details about the returned values.
|
| 397 |
+
"""
|
| 398 |
+
|
| 399 |
+
__slots__ = (
|
| 400 |
+
"_working_dir",
|
| 401 |
+
"cat_file_all",
|
| 402 |
+
"cat_file_header",
|
| 403 |
+
"_version_info",
|
| 404 |
+
"_version_info_token",
|
| 405 |
+
"_git_options",
|
| 406 |
+
"_persistent_git_options",
|
| 407 |
+
"_environment",
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
_excluded_ = (
|
| 411 |
+
"cat_file_all",
|
| 412 |
+
"cat_file_header",
|
| 413 |
+
"_version_info",
|
| 414 |
+
"_version_info_token",
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
re_unsafe_protocol = re.compile(r"(.+)::.+")
|
| 418 |
+
|
| 419 |
+
def __getstate__(self) -> Dict[str, Any]:
|
| 420 |
+
return slots_to_dict(self, exclude=self._excluded_)
|
| 421 |
+
|
| 422 |
+
def __setstate__(self, d: Dict[str, Any]) -> None:
|
| 423 |
+
dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_)
|
| 424 |
+
|
| 425 |
+
# CONFIGURATION
|
| 426 |
+
|
| 427 |
+
git_exec_name = "git"
|
| 428 |
+
"""Default git command that should work on Linux, Windows, and other systems."""
|
| 429 |
+
|
| 430 |
+
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
|
| 431 |
+
"""Enables debugging of GitPython's git commands."""
|
| 432 |
+
|
| 433 |
+
USE_SHELL: bool = False
|
| 434 |
+
"""Deprecated. If set to ``True``, a shell will be used when executing git commands.
|
| 435 |
+
|
| 436 |
+
Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
|
| 437 |
+
functions should be updated to use the default value of ``False`` instead. ``True``
|
| 438 |
+
is unsafe unless the effect of syntax treated specially by the shell is fully
|
| 439 |
+
considered and accounted for, which is not possible under most circumstances. As
|
| 440 |
+
detailed below, it is also no longer needed, even where it had been in the past.
|
| 441 |
+
|
| 442 |
+
It is in many if not most cases a command injection vulnerability for an application
|
| 443 |
+
to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
|
| 444 |
+
fragment of text to make its way into any part of any argument to any git command
|
| 445 |
+
(including paths, branch names, etc.) can cause the shell to read and write
|
| 446 |
+
arbitrary files and execute arbitrary commands. Innocent input may also accidentally
|
| 447 |
+
contain special shell syntax, leading to inadvertent malfunctions.
|
| 448 |
+
|
| 449 |
+
In addition, how a value of ``True`` interacts with some aspects of GitPython's
|
| 450 |
+
operation is not precisely specified and may change without warning, even before
|
| 451 |
+
GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
|
| 452 |
+
|
| 453 |
+
* Whether or how GitPython automatically customizes the shell environment.
|
| 454 |
+
|
| 455 |
+
* Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
|
| 456 |
+
separate arguments even when ``shell=True``), this can be used with any GitPython
|
| 457 |
+
functionality other than direct calls to the :meth:`execute` method.
|
| 458 |
+
|
| 459 |
+
* Whether any GitPython feature that runs git commands ever attempts to partially
|
| 460 |
+
sanitize data a shell may treat specially. Currently this is not done.
|
| 461 |
+
|
| 462 |
+
Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
|
| 463 |
+
in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
|
| 464 |
+
GitPython solves that problem more robustly and safely by using the
|
| 465 |
+
``CREATE_NO_WINDOW`` process creation flag on Windows.
|
| 466 |
+
|
| 467 |
+
Because Windows path search differs subtly based on whether a shell is used, in rare
|
| 468 |
+
cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
|
| 469 |
+
such as a batch file, from being found. To fix this, set the command name or full
|
| 470 |
+
path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
|
| 471 |
+
full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
|
| 472 |
+
|
| 473 |
+
Further reading:
|
| 474 |
+
|
| 475 |
+
* :meth:`Git.execute` (on the ``shell`` parameter).
|
| 476 |
+
* https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
|
| 477 |
+
* https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
|
| 478 |
+
* https://github.com/python/cpython/issues/91558#issuecomment-1100942950
|
| 479 |
+
* https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
|
| 480 |
+
"""
|
| 481 |
+
|
| 482 |
+
_git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
|
| 483 |
+
_refresh_env_var = "GIT_PYTHON_REFRESH"
|
| 484 |
+
|
| 485 |
+
GIT_PYTHON_GIT_EXECUTABLE = None
|
| 486 |
+
"""Provide the full path to the git executable. Otherwise it assumes git is in the
|
| 487 |
+
executable search path.
|
| 488 |
+
|
| 489 |
+
:note:
|
| 490 |
+
The git executable is actually found during the refresh step in the top level
|
| 491 |
+
``__init__``. It can also be changed by explicitly calling :func:`git.refresh`.
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
_refresh_token = object() # Since None would match an initial _version_info_token.
|
| 495 |
+
|
| 496 |
+
@classmethod
|
| 497 |
+
def refresh(cls, path: Union[None, PathLike] = None) -> bool:
|
| 498 |
+
"""Update information about the git executable :class:`Git` objects will use.
|
| 499 |
+
|
| 500 |
+
Called by the :func:`git.refresh` function in the top level ``__init__``.
|
| 501 |
+
|
| 502 |
+
:param path:
|
| 503 |
+
Optional path to the git executable. If not absolute, it is resolved
|
| 504 |
+
immediately, relative to the current directory. (See note below.)
|
| 505 |
+
|
| 506 |
+
:note:
|
| 507 |
+
The top-level :func:`git.refresh` should be preferred because it calls this
|
| 508 |
+
method and may also update other state accordingly.
|
| 509 |
+
|
| 510 |
+
:note:
|
| 511 |
+
There are three different ways to specify the command that refreshing causes
|
| 512 |
+
to be used for git:
|
| 513 |
+
|
| 514 |
+
1. Pass no `path` argument and do not set the
|
| 515 |
+
:envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable. The command
|
| 516 |
+
name ``git`` is used. It is looked up in a path search by the system, in
|
| 517 |
+
each command run (roughly similar to how git is found when running
|
| 518 |
+
``git`` commands manually). This is usually the desired behavior.
|
| 519 |
+
|
| 520 |
+
2. Pass no `path` argument but set the :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
|
| 521 |
+
environment variable. The command given as the value of that variable is
|
| 522 |
+
used. This may be a simple command or an arbitrary path. It is looked up
|
| 523 |
+
in each command run. Setting :envvar:`GIT_PYTHON_GIT_EXECUTABLE` to
|
| 524 |
+
``git`` has the same effect as not setting it.
|
| 525 |
+
|
| 526 |
+
3. Pass a `path` argument. This path, if not absolute, is immediately
|
| 527 |
+
resolved, relative to the current directory. This resolution occurs at
|
| 528 |
+
the time of the refresh. When git commands are run, they are run using
|
| 529 |
+
that previously resolved path. If a `path` argument is passed, the
|
| 530 |
+
:envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable is not
|
| 531 |
+
consulted.
|
| 532 |
+
|
| 533 |
+
:note:
|
| 534 |
+
Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
|
| 535 |
+
attribute, which can be read on the :class:`Git` class or any of its
|
| 536 |
+
instances to check what command is used to run git. This attribute should
|
| 537 |
+
not be confused with the related :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
|
| 538 |
+
environment variable. The class attribute is set no matter how refreshing is
|
| 539 |
+
performed.
|
| 540 |
+
"""
|
| 541 |
+
# Discern which path to refresh with.
|
| 542 |
+
if path is not None:
|
| 543 |
+
new_git = os.path.expanduser(path)
|
| 544 |
+
new_git = os.path.abspath(new_git)
|
| 545 |
+
else:
|
| 546 |
+
new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name)
|
| 547 |
+
|
| 548 |
+
# Keep track of the old and new git executable path.
|
| 549 |
+
old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
|
| 550 |
+
old_refresh_token = cls._refresh_token
|
| 551 |
+
cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
|
| 552 |
+
cls._refresh_token = object()
|
| 553 |
+
|
| 554 |
+
# Test if the new git executable path is valid. A GitCommandNotFound error is
|
| 555 |
+
# raised by us. A PermissionError is raised if the git executable cannot be
|
| 556 |
+
# executed for whatever reason.
|
| 557 |
+
has_git = False
|
| 558 |
+
try:
|
| 559 |
+
cls().version()
|
| 560 |
+
has_git = True
|
| 561 |
+
except (GitCommandNotFound, PermissionError):
|
| 562 |
+
pass
|
| 563 |
+
|
| 564 |
+
# Warn or raise exception if test failed.
|
| 565 |
+
if not has_git:
|
| 566 |
+
err = (
|
| 567 |
+
dedent(
|
| 568 |
+
"""\
|
| 569 |
+
Bad git executable.
|
| 570 |
+
The git executable must be specified in one of the following ways:
|
| 571 |
+
- be included in your $PATH
|
| 572 |
+
- be set via $%s
|
| 573 |
+
- explicitly set via git.refresh(<full-path-to-git-executable>)
|
| 574 |
+
"""
|
| 575 |
+
)
|
| 576 |
+
% cls._git_exec_env_var
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
# Revert to whatever the old_git was.
|
| 580 |
+
cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
|
| 581 |
+
cls._refresh_token = old_refresh_token
|
| 582 |
+
|
| 583 |
+
if old_git is None:
|
| 584 |
+
# On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
|
| 585 |
+
# are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
|
| 586 |
+
|
| 587 |
+
# Determine what the user wants to happen during the initial refresh. We
|
| 588 |
+
# expect GIT_PYTHON_REFRESH to either be unset or be one of the
|
| 589 |
+
# following values:
|
| 590 |
+
#
|
| 591 |
+
# 0|q|quiet|s|silence|silent|n|none
|
| 592 |
+
# 1|w|warn|warning|l|log
|
| 593 |
+
# 2|r|raise|e|error|exception
|
| 594 |
+
|
| 595 |
+
mode = os.environ.get(cls._refresh_env_var, "raise").lower()
|
| 596 |
+
|
| 597 |
+
quiet = ["quiet", "q", "silence", "s", "silent", "none", "n", "0"]
|
| 598 |
+
warn = ["warn", "w", "warning", "log", "l", "1"]
|
| 599 |
+
error = ["error", "e", "exception", "raise", "r", "2"]
|
| 600 |
+
|
| 601 |
+
if mode in quiet:
|
| 602 |
+
pass
|
| 603 |
+
elif mode in warn or mode in error:
|
| 604 |
+
err = dedent(
|
| 605 |
+
"""\
|
| 606 |
+
%s
|
| 607 |
+
All git commands will error until this is rectified.
|
| 608 |
+
|
| 609 |
+
This initial message can be silenced or aggravated in the future by setting the
|
| 610 |
+
$%s environment variable. Use one of the following values:
|
| 611 |
+
- %s: for no message or exception
|
| 612 |
+
- %s: for a warning message (logging level CRITICAL, displayed by default)
|
| 613 |
+
- %s: for a raised exception
|
| 614 |
+
|
| 615 |
+
Example:
|
| 616 |
+
export %s=%s
|
| 617 |
+
"""
|
| 618 |
+
) % (
|
| 619 |
+
err,
|
| 620 |
+
cls._refresh_env_var,
|
| 621 |
+
"|".join(quiet),
|
| 622 |
+
"|".join(warn),
|
| 623 |
+
"|".join(error),
|
| 624 |
+
cls._refresh_env_var,
|
| 625 |
+
quiet[0],
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
if mode in warn:
|
| 629 |
+
_logger.critical(err)
|
| 630 |
+
else:
|
| 631 |
+
raise ImportError(err)
|
| 632 |
+
else:
|
| 633 |
+
err = dedent(
|
| 634 |
+
"""\
|
| 635 |
+
%s environment variable has been set but it has been set with an invalid value.
|
| 636 |
+
|
| 637 |
+
Use only the following values:
|
| 638 |
+
- %s: for no message or exception
|
| 639 |
+
- %s: for a warning message (logging level CRITICAL, displayed by default)
|
| 640 |
+
- %s: for a raised exception
|
| 641 |
+
"""
|
| 642 |
+
) % (
|
| 643 |
+
cls._refresh_env_var,
|
| 644 |
+
"|".join(quiet),
|
| 645 |
+
"|".join(warn),
|
| 646 |
+
"|".join(error),
|
| 647 |
+
)
|
| 648 |
+
raise ImportError(err)
|
| 649 |
+
|
| 650 |
+
# We get here if this was the initial refresh and the refresh mode was
|
| 651 |
+
# not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
|
| 652 |
+
# discern the difference between the first refresh at import time
|
| 653 |
+
# and subsequent calls to git.refresh or this refresh method.
|
| 654 |
+
cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
|
| 655 |
+
else:
|
| 656 |
+
# After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
|
| 657 |
+
# None) we raise an exception.
|
| 658 |
+
raise GitCommandNotFound(new_git, err)
|
| 659 |
+
|
| 660 |
+
return has_git
|
| 661 |
+
|
| 662 |
+
@classmethod
|
| 663 |
+
def is_cygwin(cls) -> bool:
|
| 664 |
+
return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE)
|
| 665 |
+
|
| 666 |
+
@overload
|
| 667 |
+
@classmethod
|
| 668 |
+
def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ...
|
| 669 |
+
|
| 670 |
+
@overload
|
| 671 |
+
@classmethod
|
| 672 |
+
def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ...
|
| 673 |
+
|
| 674 |
+
@classmethod
|
| 675 |
+
def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
|
| 676 |
+
"""Remove any backslashes from URLs to be written in config files.
|
| 677 |
+
|
| 678 |
+
Windows might create config files containing paths with backslashes, but git
|
| 679 |
+
stops liking them as it will escape the backslashes. Hence we undo the escaping
|
| 680 |
+
just to be sure.
|
| 681 |
+
"""
|
| 682 |
+
if is_cygwin is None:
|
| 683 |
+
is_cygwin = cls.is_cygwin()
|
| 684 |
+
|
| 685 |
+
if is_cygwin:
|
| 686 |
+
url = cygpath(url)
|
| 687 |
+
else:
|
| 688 |
+
url = os.path.expandvars(url)
|
| 689 |
+
if url.startswith("~"):
|
| 690 |
+
url = os.path.expanduser(url)
|
| 691 |
+
url = url.replace("\\\\", "\\").replace("\\", "/")
|
| 692 |
+
return url
|
| 693 |
+
|
| 694 |
+
@classmethod
|
| 695 |
+
def check_unsafe_protocols(cls, url: str) -> None:
|
| 696 |
+
"""Check for unsafe protocols.
|
| 697 |
+
|
| 698 |
+
Apart from the usual protocols (http, git, ssh), Git allows "remote helpers"
|
| 699 |
+
that have the form ``<transport>::<address>``. One of these helpers (``ext::``)
|
| 700 |
+
can be used to invoke any arbitrary command.
|
| 701 |
+
|
| 702 |
+
See:
|
| 703 |
+
|
| 704 |
+
- https://git-scm.com/docs/gitremote-helpers
|
| 705 |
+
- https://git-scm.com/docs/git-remote-ext
|
| 706 |
+
"""
|
| 707 |
+
match = cls.re_unsafe_protocol.match(url)
|
| 708 |
+
if match:
|
| 709 |
+
protocol = match.group(1)
|
| 710 |
+
raise UnsafeProtocolError(
|
| 711 |
+
f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
@classmethod
|
| 715 |
+
def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
|
| 716 |
+
"""Check for unsafe options.
|
| 717 |
+
|
| 718 |
+
Some options that are passed to ``git <command>`` can be used to execute
|
| 719 |
+
arbitrary commands. These are blocked by default.
|
| 720 |
+
"""
|
| 721 |
+
# Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
|
| 722 |
+
# check if they start with "--foo" or if they are equal to "foo".
|
| 723 |
+
bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
|
| 724 |
+
for option in options:
|
| 725 |
+
for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
|
| 726 |
+
if option.startswith(unsafe_option) or option == bare_option:
|
| 727 |
+
raise UnsafeOptionError(
|
| 728 |
+
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
class AutoInterrupt:
|
| 732 |
+
"""Process wrapper that terminates the wrapped process on finalization.
|
| 733 |
+
|
| 734 |
+
This kills/interrupts the stored process instance once this instance goes out of
|
| 735 |
+
scope. It is used to prevent processes piling up in case iterators stop reading.
|
| 736 |
+
|
| 737 |
+
All attributes are wired through to the contained process object.
|
| 738 |
+
|
| 739 |
+
The wait method is overridden to perform automatic status code checking and
|
| 740 |
+
possibly raise.
|
| 741 |
+
"""
|
| 742 |
+
|
| 743 |
+
__slots__ = ("proc", "args", "status")
|
| 744 |
+
|
| 745 |
+
# If this is non-zero it will override any status code during _terminate, used
|
| 746 |
+
# to prevent race conditions in testing.
|
| 747 |
+
_status_code_if_terminate: int = 0
|
| 748 |
+
|
| 749 |
+
def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
|
| 750 |
+
self.proc = proc
|
| 751 |
+
self.args = args
|
| 752 |
+
self.status: Union[int, None] = None
|
| 753 |
+
|
| 754 |
+
def _terminate(self) -> None:
|
| 755 |
+
"""Terminate the underlying process."""
|
| 756 |
+
if self.proc is None:
|
| 757 |
+
return
|
| 758 |
+
|
| 759 |
+
proc = self.proc
|
| 760 |
+
self.proc = None
|
| 761 |
+
if proc.stdin:
|
| 762 |
+
proc.stdin.close()
|
| 763 |
+
if proc.stdout:
|
| 764 |
+
proc.stdout.close()
|
| 765 |
+
if proc.stderr:
|
| 766 |
+
proc.stderr.close()
|
| 767 |
+
# Did the process finish already so we have a return code?
|
| 768 |
+
try:
|
| 769 |
+
if proc.poll() is not None:
|
| 770 |
+
self.status = self._status_code_if_terminate or proc.poll()
|
| 771 |
+
return
|
| 772 |
+
except OSError as ex:
|
| 773 |
+
_logger.info("Ignored error after process had died: %r", ex)
|
| 774 |
+
|
| 775 |
+
# It can be that nothing really exists anymore...
|
| 776 |
+
if os is None or getattr(os, "kill", None) is None:
|
| 777 |
+
return
|
| 778 |
+
|
| 779 |
+
# Try to kill it.
|
| 780 |
+
try:
|
| 781 |
+
proc.terminate()
|
| 782 |
+
status = proc.wait() # Ensure the process goes away.
|
| 783 |
+
|
| 784 |
+
self.status = self._status_code_if_terminate or status
|
| 785 |
+
except OSError as ex:
|
| 786 |
+
_logger.info("Ignored error after process had died: %r", ex)
|
| 787 |
+
# END exception handling
|
| 788 |
+
|
| 789 |
+
def __del__(self) -> None:
|
| 790 |
+
self._terminate()
|
| 791 |
+
|
| 792 |
+
def __getattr__(self, attr: str) -> Any:
|
| 793 |
+
return getattr(self.proc, attr)
|
| 794 |
+
|
| 795 |
+
# TODO: Bad choice to mimic `proc.wait()` but with different args.
|
| 796 |
+
def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
|
| 797 |
+
"""Wait for the process and return its status code.
|
| 798 |
+
|
| 799 |
+
:param stderr:
|
| 800 |
+
Previously read value of stderr, in case stderr is already closed.
|
| 801 |
+
|
| 802 |
+
:warn:
|
| 803 |
+
May deadlock if output or error pipes are used and not handled
|
| 804 |
+
separately.
|
| 805 |
+
|
| 806 |
+
:raise git.exc.GitCommandError:
|
| 807 |
+
If the return status is not 0.
|
| 808 |
+
"""
|
| 809 |
+
if stderr is None:
|
| 810 |
+
stderr_b = b""
|
| 811 |
+
stderr_b = force_bytes(data=stderr, encoding="utf-8")
|
| 812 |
+
status: Union[int, None]
|
| 813 |
+
if self.proc is not None:
|
| 814 |
+
status = self.proc.wait()
|
| 815 |
+
p_stderr = self.proc.stderr
|
| 816 |
+
else: # Assume the underlying proc was killed earlier or never existed.
|
| 817 |
+
status = self.status
|
| 818 |
+
p_stderr = None
|
| 819 |
+
|
| 820 |
+
def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
|
| 821 |
+
if stream:
|
| 822 |
+
try:
|
| 823 |
+
return stderr_b + force_bytes(stream.read())
|
| 824 |
+
except (OSError, ValueError):
|
| 825 |
+
return stderr_b or b""
|
| 826 |
+
else:
|
| 827 |
+
return stderr_b or b""
|
| 828 |
+
|
| 829 |
+
# END status handling
|
| 830 |
+
|
| 831 |
+
if status != 0:
|
| 832 |
+
errstr = read_all_from_possibly_closed_stream(p_stderr)
|
| 833 |
+
_logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
|
| 834 |
+
raise GitCommandError(remove_password_if_present(self.args), status, errstr)
|
| 835 |
+
return status
|
| 836 |
+
|
| 837 |
+
# END auto interrupt
|
| 838 |
+
|
| 839 |
+
class CatFileContentStream:
|
| 840 |
+
"""Object representing a sized read-only stream returning the contents of
|
| 841 |
+
an object.
|
| 842 |
+
|
| 843 |
+
This behaves like a stream, but counts the data read and simulates an empty
|
| 844 |
+
stream once our sized content region is empty.
|
| 845 |
+
|
| 846 |
+
If not all data are read to the end of the object's lifetime, we read the
|
| 847 |
+
rest to ensure the underlying stream continues to work.
|
| 848 |
+
"""
|
| 849 |
+
|
| 850 |
+
__slots__ = ("_stream", "_nbr", "_size")
|
| 851 |
+
|
| 852 |
+
def __init__(self, size: int, stream: IO[bytes]) -> None:
|
| 853 |
+
self._stream = stream
|
| 854 |
+
self._size = size
|
| 855 |
+
self._nbr = 0 # Number of bytes read.
|
| 856 |
+
|
| 857 |
+
# Special case: If the object is empty, has null bytes, get the final
|
| 858 |
+
# newline right away.
|
| 859 |
+
if size == 0:
|
| 860 |
+
stream.read(1)
|
| 861 |
+
# END handle empty streams
|
| 862 |
+
|
| 863 |
+
def read(self, size: int = -1) -> bytes:
|
| 864 |
+
bytes_left = self._size - self._nbr
|
| 865 |
+
if bytes_left == 0:
|
| 866 |
+
return b""
|
| 867 |
+
if size > -1:
|
| 868 |
+
# Ensure we don't try to read past our limit.
|
| 869 |
+
size = min(bytes_left, size)
|
| 870 |
+
else:
|
| 871 |
+
# They try to read all, make sure it's not more than what remains.
|
| 872 |
+
size = bytes_left
|
| 873 |
+
# END check early depletion
|
| 874 |
+
data = self._stream.read(size)
|
| 875 |
+
self._nbr += len(data)
|
| 876 |
+
|
| 877 |
+
# Check for depletion, read our final byte to make the stream usable by
|
| 878 |
+
# others.
|
| 879 |
+
if self._size - self._nbr == 0:
|
| 880 |
+
self._stream.read(1) # final newline
|
| 881 |
+
# END finish reading
|
| 882 |
+
return data
|
| 883 |
+
|
| 884 |
+
def readline(self, size: int = -1) -> bytes:
|
| 885 |
+
if self._nbr == self._size:
|
| 886 |
+
return b""
|
| 887 |
+
|
| 888 |
+
# Clamp size to lowest allowed value.
|
| 889 |
+
bytes_left = self._size - self._nbr
|
| 890 |
+
if size > -1:
|
| 891 |
+
size = min(bytes_left, size)
|
| 892 |
+
else:
|
| 893 |
+
size = bytes_left
|
| 894 |
+
# END handle size
|
| 895 |
+
|
| 896 |
+
data = self._stream.readline(size)
|
| 897 |
+
self._nbr += len(data)
|
| 898 |
+
|
| 899 |
+
# Handle final byte.
|
| 900 |
+
if self._size - self._nbr == 0:
|
| 901 |
+
self._stream.read(1)
|
| 902 |
+
# END finish reading
|
| 903 |
+
|
| 904 |
+
return data
|
| 905 |
+
|
| 906 |
+
def readlines(self, size: int = -1) -> List[bytes]:
|
| 907 |
+
if self._nbr == self._size:
|
| 908 |
+
return []
|
| 909 |
+
|
| 910 |
+
# Leave all additional logic to our readline method, we just check the size.
|
| 911 |
+
out = []
|
| 912 |
+
nbr = 0
|
| 913 |
+
while True:
|
| 914 |
+
line = self.readline()
|
| 915 |
+
if not line:
|
| 916 |
+
break
|
| 917 |
+
out.append(line)
|
| 918 |
+
if size > -1:
|
| 919 |
+
nbr += len(line)
|
| 920 |
+
if nbr > size:
|
| 921 |
+
break
|
| 922 |
+
# END handle size constraint
|
| 923 |
+
# END readline loop
|
| 924 |
+
return out
|
| 925 |
+
|
| 926 |
+
# skipcq: PYL-E0301
|
| 927 |
+
def __iter__(self) -> "Git.CatFileContentStream":
|
| 928 |
+
return self
|
| 929 |
+
|
| 930 |
+
def __next__(self) -> bytes:
|
| 931 |
+
line = self.readline()
|
| 932 |
+
if not line:
|
| 933 |
+
raise StopIteration
|
| 934 |
+
|
| 935 |
+
return line
|
| 936 |
+
|
| 937 |
+
next = __next__
|
| 938 |
+
|
| 939 |
+
def __del__(self) -> None:
|
| 940 |
+
bytes_left = self._size - self._nbr
|
| 941 |
+
if bytes_left:
|
| 942 |
+
# Read and discard - seeking is impossible within a stream.
|
| 943 |
+
# This includes any terminating newline.
|
| 944 |
+
self._stream.read(bytes_left + 1)
|
| 945 |
+
# END handle incomplete read
|
| 946 |
+
|
| 947 |
+
def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
|
| 948 |
+
"""Initialize this instance with:
|
| 949 |
+
|
| 950 |
+
:param working_dir:
|
| 951 |
+
Git directory we should work in. If ``None``, we always work in the current
|
| 952 |
+
directory as returned by :func:`os.getcwd`.
|
| 953 |
+
This is meant to be the working tree directory if available, or the
|
| 954 |
+
``.git`` directory in case of bare repositories.
|
| 955 |
+
"""
|
| 956 |
+
super().__init__()
|
| 957 |
+
self._working_dir = expand_path(working_dir)
|
| 958 |
+
self._git_options: Union[List[str], Tuple[str, ...]] = ()
|
| 959 |
+
self._persistent_git_options: List[str] = []
|
| 960 |
+
|
| 961 |
+
# Extra environment variables to pass to git commands
|
| 962 |
+
self._environment: Dict[str, str] = {}
|
| 963 |
+
|
| 964 |
+
# Cached version slots
|
| 965 |
+
self._version_info: Union[Tuple[int, ...], None] = None
|
| 966 |
+
self._version_info_token: object = None
|
| 967 |
+
|
| 968 |
+
# Cached command slots
|
| 969 |
+
self.cat_file_header: Union[None, TBD] = None
|
| 970 |
+
self.cat_file_all: Union[None, TBD] = None
|
| 971 |
+
|
| 972 |
+
def __getattribute__(self, name: str) -> Any:
|
| 973 |
+
if name == "USE_SHELL":
|
| 974 |
+
_warn_use_shell(False)
|
| 975 |
+
return super().__getattribute__(name)
|
| 976 |
+
|
| 977 |
+
def __getattr__(self, name: str) -> Any:
|
| 978 |
+
"""A convenience method as it allows to call the command as if it was an object.
|
| 979 |
+
|
| 980 |
+
:return:
|
| 981 |
+
Callable object that will execute call :meth:`_call_process` with your
|
| 982 |
+
arguments.
|
| 983 |
+
"""
|
| 984 |
+
if name.startswith("_"):
|
| 985 |
+
return super().__getattribute__(name)
|
| 986 |
+
return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
|
| 987 |
+
|
| 988 |
+
def set_persistent_git_options(self, **kwargs: Any) -> None:
|
| 989 |
+
"""Specify command line options to the git executable for subsequent
|
| 990 |
+
subcommand calls.
|
| 991 |
+
|
| 992 |
+
:param kwargs:
|
| 993 |
+
A dict of keyword arguments.
|
| 994 |
+
These arguments are passed as in :meth:`_call_process`, but will be passed
|
| 995 |
+
to the git command rather than the subcommand.
|
| 996 |
+
"""
|
| 997 |
+
|
| 998 |
+
self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
|
| 999 |
+
|
| 1000 |
+
@property
|
| 1001 |
+
def working_dir(self) -> Union[None, PathLike]:
|
| 1002 |
+
""":return: Git directory we are working on"""
|
| 1003 |
+
return self._working_dir
|
| 1004 |
+
|
| 1005 |
+
@property
|
| 1006 |
+
def version_info(self) -> Tuple[int, ...]:
|
| 1007 |
+
"""
|
| 1008 |
+
:return: Tuple with integers representing the major, minor and additional
|
| 1009 |
+
version numbers as parsed from :manpage:`git-version(1)`. Up to four fields
|
| 1010 |
+
are used.
|
| 1011 |
+
|
| 1012 |
+
This value is generated on demand and is cached.
|
| 1013 |
+
"""
|
| 1014 |
+
# Refreshing is global, but version_info caching is per-instance.
|
| 1015 |
+
refresh_token = self._refresh_token # Copy token in case of concurrent refresh.
|
| 1016 |
+
|
| 1017 |
+
# Use the cached version if obtained after the most recent refresh.
|
| 1018 |
+
if self._version_info_token is refresh_token:
|
| 1019 |
+
assert self._version_info is not None, "Bug: corrupted token-check state"
|
| 1020 |
+
return self._version_info
|
| 1021 |
+
|
| 1022 |
+
# Run "git version" and parse it.
|
| 1023 |
+
process_version = self._call_process("version")
|
| 1024 |
+
version_string = process_version.split(" ")[2]
|
| 1025 |
+
version_fields = version_string.split(".")[:4]
|
| 1026 |
+
leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields)
|
| 1027 |
+
self._version_info = tuple(map(int, leading_numeric_fields))
|
| 1028 |
+
|
| 1029 |
+
# This value will be considered valid until the next refresh.
|
| 1030 |
+
self._version_info_token = refresh_token
|
| 1031 |
+
return self._version_info
|
| 1032 |
+
|
| 1033 |
+
@overload
|
| 1034 |
+
def execute(
|
| 1035 |
+
self,
|
| 1036 |
+
command: Union[str, Sequence[Any]],
|
| 1037 |
+
*,
|
| 1038 |
+
as_process: Literal[True],
|
| 1039 |
+
) -> "AutoInterrupt": ...
|
| 1040 |
+
|
| 1041 |
+
@overload
|
| 1042 |
+
def execute(
|
| 1043 |
+
self,
|
| 1044 |
+
command: Union[str, Sequence[Any]],
|
| 1045 |
+
*,
|
| 1046 |
+
as_process: Literal[False] = False,
|
| 1047 |
+
stdout_as_string: Literal[True],
|
| 1048 |
+
) -> Union[str, Tuple[int, str, str]]: ...
|
| 1049 |
+
|
| 1050 |
+
@overload
|
| 1051 |
+
def execute(
|
| 1052 |
+
self,
|
| 1053 |
+
command: Union[str, Sequence[Any]],
|
| 1054 |
+
*,
|
| 1055 |
+
as_process: Literal[False] = False,
|
| 1056 |
+
stdout_as_string: Literal[False] = False,
|
| 1057 |
+
) -> Union[bytes, Tuple[int, bytes, str]]: ...
|
| 1058 |
+
|
| 1059 |
+
@overload
|
| 1060 |
+
def execute(
|
| 1061 |
+
self,
|
| 1062 |
+
command: Union[str, Sequence[Any]],
|
| 1063 |
+
*,
|
| 1064 |
+
with_extended_output: Literal[False],
|
| 1065 |
+
as_process: Literal[False],
|
| 1066 |
+
stdout_as_string: Literal[True],
|
| 1067 |
+
) -> str: ...
|
| 1068 |
+
|
| 1069 |
+
@overload
|
| 1070 |
+
def execute(
|
| 1071 |
+
self,
|
| 1072 |
+
command: Union[str, Sequence[Any]],
|
| 1073 |
+
*,
|
| 1074 |
+
with_extended_output: Literal[False],
|
| 1075 |
+
as_process: Literal[False],
|
| 1076 |
+
stdout_as_string: Literal[False],
|
| 1077 |
+
) -> bytes: ...
|
| 1078 |
+
|
| 1079 |
+
def execute(
|
| 1080 |
+
self,
|
| 1081 |
+
command: Union[str, Sequence[Any]],
|
| 1082 |
+
istream: Union[None, BinaryIO] = None,
|
| 1083 |
+
with_extended_output: bool = False,
|
| 1084 |
+
with_exceptions: bool = True,
|
| 1085 |
+
as_process: bool = False,
|
| 1086 |
+
output_stream: Union[None, BinaryIO] = None,
|
| 1087 |
+
stdout_as_string: bool = True,
|
| 1088 |
+
kill_after_timeout: Union[None, float] = None,
|
| 1089 |
+
with_stdout: bool = True,
|
| 1090 |
+
universal_newlines: bool = False,
|
| 1091 |
+
shell: Union[None, bool] = None,
|
| 1092 |
+
env: Union[None, Mapping[str, str]] = None,
|
| 1093 |
+
max_chunk_size: int = io.DEFAULT_BUFFER_SIZE,
|
| 1094 |
+
strip_newline_in_stdout: bool = True,
|
| 1095 |
+
**subprocess_kwargs: Any,
|
| 1096 |
+
) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
|
| 1097 |
+
R"""Handle executing the command, and consume and return the returned
|
| 1098 |
+
information (stdout).
|
| 1099 |
+
|
| 1100 |
+
:param command:
|
| 1101 |
+
The command argument list to execute.
|
| 1102 |
+
It should be a sequence of program arguments, or a string. The
|
| 1103 |
+
program to execute is the first item in the args sequence or string.
|
| 1104 |
+
|
| 1105 |
+
:param istream:
|
| 1106 |
+
Standard input filehandle passed to :class:`subprocess.Popen`.
|
| 1107 |
+
|
| 1108 |
+
:param with_extended_output:
|
| 1109 |
+
Whether to return a (status, stdout, stderr) tuple.
|
| 1110 |
+
|
| 1111 |
+
:param with_exceptions:
|
| 1112 |
+
Whether to raise an exception when git returns a non-zero status.
|
| 1113 |
+
|
| 1114 |
+
:param as_process:
|
| 1115 |
+
Whether to return the created process instance directly from which
|
| 1116 |
+
streams can be read on demand. This will render `with_extended_output`
|
| 1117 |
+
and `with_exceptions` ineffective - the caller will have to deal with
|
| 1118 |
+
the details. It is important to note that the process will be placed
|
| 1119 |
+
into an :class:`AutoInterrupt` wrapper that will interrupt the process
|
| 1120 |
+
once it goes out of scope. If you use the command in iterators, you
|
| 1121 |
+
should pass the whole process instance instead of a single stream.
|
| 1122 |
+
|
| 1123 |
+
:param output_stream:
|
| 1124 |
+
If set to a file-like object, data produced by the git command will be
|
| 1125 |
+
copied to the given stream instead of being returned as a string.
|
| 1126 |
+
This feature only has any effect if `as_process` is ``False``.
|
| 1127 |
+
|
| 1128 |
+
:param stdout_as_string:
|
| 1129 |
+
If ``False``, the command's standard output will be bytes. Otherwise, it
|
| 1130 |
+
will be decoded into a string using the default encoding (usually UTF-8).
|
| 1131 |
+
The latter can fail, if the output contains binary data.
|
| 1132 |
+
|
| 1133 |
+
:param kill_after_timeout:
|
| 1134 |
+
Specifies a timeout in seconds for the git command, after which the process
|
| 1135 |
+
should be killed. This will have no effect if `as_process` is set to
|
| 1136 |
+
``True``. It is set to ``None`` by default and will let the process run
|
| 1137 |
+
until the timeout is explicitly specified. Uses of this feature should be
|
| 1138 |
+
carefully considered, due to the following limitations:
|
| 1139 |
+
|
| 1140 |
+
1. This feature is not supported at all on Windows.
|
| 1141 |
+
2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
|
| 1142 |
+
enumerate child processes, which is available on most GNU/Linux systems
|
| 1143 |
+
but not most others.
|
| 1144 |
+
3. Deeper descendants do not receive signals, though they may sometimes
|
| 1145 |
+
terminate as a consequence of their parent processes being killed.
|
| 1146 |
+
4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side
|
| 1147 |
+
effects on a repository. For example, stale locks in case of
|
| 1148 |
+
:manpage:`git-gc(1)` could render the repository incapable of accepting
|
| 1149 |
+
changes until the lock is manually removed.
|
| 1150 |
+
|
| 1151 |
+
:param with_stdout:
|
| 1152 |
+
If ``True``, default ``True``, we open stdout on the created process.
|
| 1153 |
+
|
| 1154 |
+
:param universal_newlines:
|
| 1155 |
+
If ``True``, pipes will be opened as text, and lines are split at all known
|
| 1156 |
+
line endings.
|
| 1157 |
+
|
| 1158 |
+
:param shell:
|
| 1159 |
+
Whether to invoke commands through a shell
|
| 1160 |
+
(see :class:`Popen(..., shell=True) <subprocess.Popen>`).
|
| 1161 |
+
If this is not ``None``, it overrides :attr:`USE_SHELL`.
|
| 1162 |
+
|
| 1163 |
+
Passing ``shell=True`` to this or any other GitPython function should be
|
| 1164 |
+
avoided, as it is unsafe under most circumstances. This is because it is
|
| 1165 |
+
typically not feasible to fully consider and account for the effect of shell
|
| 1166 |
+
expansions, especially when passing ``shell=True`` to other methods that
|
| 1167 |
+
forward it to :meth:`Git.execute`. Passing ``shell=True`` is also no longer
|
| 1168 |
+
needed (nor useful) to work around any known operating system specific
|
| 1169 |
+
issues.
|
| 1170 |
+
|
| 1171 |
+
:param env:
|
| 1172 |
+
A dictionary of environment variables to be passed to
|
| 1173 |
+
:class:`subprocess.Popen`.
|
| 1174 |
+
|
| 1175 |
+
:param max_chunk_size:
|
| 1176 |
+
Maximum number of bytes in one chunk of data passed to the `output_stream`
|
| 1177 |
+
in one invocation of its ``write()`` method. If the given number is not
|
| 1178 |
+
positive then the default value is used.
|
| 1179 |
+
|
| 1180 |
+
:param strip_newline_in_stdout:
|
| 1181 |
+
Whether to strip the trailing ``\n`` of the command stdout.
|
| 1182 |
+
|
| 1183 |
+
:param subprocess_kwargs:
|
| 1184 |
+
Keyword arguments to be passed to :class:`subprocess.Popen`. Please note
|
| 1185 |
+
that some of the valid kwargs are already set by this method; the ones you
|
| 1186 |
+
specify may not be the same ones.
|
| 1187 |
+
|
| 1188 |
+
:return:
|
| 1189 |
+
* str(output), if `extended_output` is ``False`` (Default)
|
| 1190 |
+
* tuple(int(status), str(stdout), str(stderr)),
|
| 1191 |
+
if `extended_output` is ``True``
|
| 1192 |
+
|
| 1193 |
+
If `output_stream` is ``True``, the stdout value will be your output stream:
|
| 1194 |
+
|
| 1195 |
+
* output_stream, if `extended_output` is ``False``
|
| 1196 |
+
* tuple(int(status), output_stream, str(stderr)),
|
| 1197 |
+
if `extended_output` is ``True``
|
| 1198 |
+
|
| 1199 |
+
Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
|
| 1200 |
+
output regardless of system language.
|
| 1201 |
+
|
| 1202 |
+
:raise git.exc.GitCommandError:
|
| 1203 |
+
|
| 1204 |
+
:note:
|
| 1205 |
+
If you add additional keyword arguments to the signature of this method, you
|
| 1206 |
+
must update the ``execute_kwargs`` variable housed in this module.
|
| 1207 |
+
"""
|
| 1208 |
+
# Remove password for the command if present.
|
| 1209 |
+
redacted_command = remove_password_if_present(command)
|
| 1210 |
+
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
|
| 1211 |
+
_logger.info(" ".join(redacted_command))
|
| 1212 |
+
|
| 1213 |
+
# Allow the user to have the command executed in their working dir.
|
| 1214 |
+
try:
|
| 1215 |
+
cwd = self._working_dir or os.getcwd() # type: Union[None, str]
|
| 1216 |
+
if not os.access(str(cwd), os.X_OK):
|
| 1217 |
+
cwd = None
|
| 1218 |
+
except FileNotFoundError:
|
| 1219 |
+
cwd = None
|
| 1220 |
+
|
| 1221 |
+
# Start the process.
|
| 1222 |
+
inline_env = env
|
| 1223 |
+
env = os.environ.copy()
|
| 1224 |
+
# Attempt to force all output to plain ASCII English, which is what some parsing
|
| 1225 |
+
# code may expect.
|
| 1226 |
+
# According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well
|
| 1227 |
+
# just to be sure.
|
| 1228 |
+
env["LANGUAGE"] = "C"
|
| 1229 |
+
env["LC_ALL"] = "C"
|
| 1230 |
+
env.update(self._environment)
|
| 1231 |
+
if inline_env is not None:
|
| 1232 |
+
env.update(inline_env)
|
| 1233 |
+
|
| 1234 |
+
if sys.platform == "win32":
|
| 1235 |
+
if kill_after_timeout is not None:
|
| 1236 |
+
raise GitCommandError(
|
| 1237 |
+
redacted_command,
|
| 1238 |
+
'"kill_after_timeout" feature is not supported on Windows.',
|
| 1239 |
+
)
|
| 1240 |
+
cmd_not_found_exception = OSError
|
| 1241 |
+
else:
|
| 1242 |
+
cmd_not_found_exception = FileNotFoundError
|
| 1243 |
+
# END handle
|
| 1244 |
+
|
| 1245 |
+
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
|
| 1246 |
+
if shell is None:
|
| 1247 |
+
# Get the value of USE_SHELL with no deprecation warning. Do this without
|
| 1248 |
+
# warnings.catch_warnings, to avoid a race condition with application code
|
| 1249 |
+
# configuring warnings. The value could be looked up in type(self).__dict__
|
| 1250 |
+
# or Git.__dict__, but those can break under some circumstances. This works
|
| 1251 |
+
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
|
| 1252 |
+
shell = super().__getattribute__("USE_SHELL")
|
| 1253 |
+
_logger.debug(
|
| 1254 |
+
"Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
|
| 1255 |
+
redacted_command,
|
| 1256 |
+
cwd,
|
| 1257 |
+
"<valid stream>" if istream else "None",
|
| 1258 |
+
shell,
|
| 1259 |
+
universal_newlines,
|
| 1260 |
+
)
|
| 1261 |
+
try:
|
| 1262 |
+
proc = safer_popen(
|
| 1263 |
+
command,
|
| 1264 |
+
env=env,
|
| 1265 |
+
cwd=cwd,
|
| 1266 |
+
bufsize=-1,
|
| 1267 |
+
stdin=(istream or DEVNULL),
|
| 1268 |
+
stderr=PIPE,
|
| 1269 |
+
stdout=stdout_sink,
|
| 1270 |
+
shell=shell,
|
| 1271 |
+
universal_newlines=universal_newlines,
|
| 1272 |
+
encoding=defenc if universal_newlines else None,
|
| 1273 |
+
**subprocess_kwargs,
|
| 1274 |
+
)
|
| 1275 |
+
except cmd_not_found_exception as err:
|
| 1276 |
+
raise GitCommandNotFound(redacted_command, err) from err
|
| 1277 |
+
else:
|
| 1278 |
+
# Replace with a typeguard for Popen[bytes]?
|
| 1279 |
+
proc.stdout = cast(BinaryIO, proc.stdout)
|
| 1280 |
+
proc.stderr = cast(BinaryIO, proc.stderr)
|
| 1281 |
+
|
| 1282 |
+
if as_process:
|
| 1283 |
+
return self.AutoInterrupt(proc, command)
|
| 1284 |
+
|
| 1285 |
+
if sys.platform != "win32" and kill_after_timeout is not None:
|
| 1286 |
+
# Help mypy figure out this is not None even when used inside communicate().
|
| 1287 |
+
timeout = kill_after_timeout
|
| 1288 |
+
|
| 1289 |
+
def kill_process(pid: int) -> None:
|
| 1290 |
+
"""Callback to kill a process.
|
| 1291 |
+
|
| 1292 |
+
This callback implementation would be ineffective and unsafe on Windows.
|
| 1293 |
+
"""
|
| 1294 |
+
p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
|
| 1295 |
+
child_pids = []
|
| 1296 |
+
if p.stdout is not None:
|
| 1297 |
+
for line in p.stdout:
|
| 1298 |
+
if len(line.split()) > 0:
|
| 1299 |
+
local_pid = (line.split())[0]
|
| 1300 |
+
if local_pid.isdigit():
|
| 1301 |
+
child_pids.append(int(local_pid))
|
| 1302 |
+
try:
|
| 1303 |
+
os.kill(pid, signal.SIGKILL)
|
| 1304 |
+
for child_pid in child_pids:
|
| 1305 |
+
try:
|
| 1306 |
+
os.kill(child_pid, signal.SIGKILL)
|
| 1307 |
+
except OSError:
|
| 1308 |
+
pass
|
| 1309 |
+
# Tell the main routine that the process was killed.
|
| 1310 |
+
kill_check.set()
|
| 1311 |
+
except OSError:
|
| 1312 |
+
# It is possible that the process gets completed in the duration
|
| 1313 |
+
# after timeout happens and before we try to kill the process.
|
| 1314 |
+
pass
|
| 1315 |
+
return
|
| 1316 |
+
|
| 1317 |
+
def communicate() -> Tuple[AnyStr, AnyStr]:
|
| 1318 |
+
watchdog.start()
|
| 1319 |
+
out, err = proc.communicate()
|
| 1320 |
+
watchdog.cancel()
|
| 1321 |
+
if kill_check.is_set():
|
| 1322 |
+
err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
|
| 1323 |
+
" ".join(redacted_command),
|
| 1324 |
+
timeout,
|
| 1325 |
+
)
|
| 1326 |
+
if not universal_newlines:
|
| 1327 |
+
err = err.encode(defenc)
|
| 1328 |
+
return out, err
|
| 1329 |
+
|
| 1330 |
+
# END helpers
|
| 1331 |
+
|
| 1332 |
+
kill_check = threading.Event()
|
| 1333 |
+
watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
|
| 1334 |
+
else:
|
| 1335 |
+
communicate = proc.communicate
|
| 1336 |
+
|
| 1337 |
+
# Wait for the process to return.
|
| 1338 |
+
status = 0
|
| 1339 |
+
stdout_value: Union[str, bytes] = b""
|
| 1340 |
+
stderr_value: Union[str, bytes] = b""
|
| 1341 |
+
newline = "\n" if universal_newlines else b"\n"
|
| 1342 |
+
try:
|
| 1343 |
+
if output_stream is None:
|
| 1344 |
+
stdout_value, stderr_value = communicate()
|
| 1345 |
+
# Strip trailing "\n".
|
| 1346 |
+
if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
|
| 1347 |
+
stdout_value = stdout_value[:-1]
|
| 1348 |
+
if stderr_value.endswith(newline): # type: ignore[arg-type]
|
| 1349 |
+
stderr_value = stderr_value[:-1]
|
| 1350 |
+
|
| 1351 |
+
status = proc.returncode
|
| 1352 |
+
else:
|
| 1353 |
+
max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
|
| 1354 |
+
stream_copy(proc.stdout, output_stream, max_chunk_size)
|
| 1355 |
+
stdout_value = proc.stdout.read()
|
| 1356 |
+
stderr_value = proc.stderr.read()
|
| 1357 |
+
# Strip trailing "\n".
|
| 1358 |
+
if stderr_value.endswith(newline): # type: ignore[arg-type]
|
| 1359 |
+
stderr_value = stderr_value[:-1]
|
| 1360 |
+
status = proc.wait()
|
| 1361 |
+
# END stdout handling
|
| 1362 |
+
finally:
|
| 1363 |
+
proc.stdout.close()
|
| 1364 |
+
proc.stderr.close()
|
| 1365 |
+
|
| 1366 |
+
if self.GIT_PYTHON_TRACE == "full":
|
| 1367 |
+
cmdstr = " ".join(redacted_command)
|
| 1368 |
+
|
| 1369 |
+
def as_text(stdout_value: Union[bytes, str]) -> str:
|
| 1370 |
+
return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
|
| 1371 |
+
|
| 1372 |
+
# END as_text
|
| 1373 |
+
|
| 1374 |
+
if stderr_value:
|
| 1375 |
+
_logger.info(
|
| 1376 |
+
"%s -> %d; stdout: '%s'; stderr: '%s'",
|
| 1377 |
+
cmdstr,
|
| 1378 |
+
status,
|
| 1379 |
+
as_text(stdout_value),
|
| 1380 |
+
safe_decode(stderr_value),
|
| 1381 |
+
)
|
| 1382 |
+
elif stdout_value:
|
| 1383 |
+
_logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
|
| 1384 |
+
else:
|
| 1385 |
+
_logger.info("%s -> %d", cmdstr, status)
|
| 1386 |
+
# END handle debug printing
|
| 1387 |
+
|
| 1388 |
+
if with_exceptions and status != 0:
|
| 1389 |
+
raise GitCommandError(redacted_command, status, stderr_value, stdout_value)
|
| 1390 |
+
|
| 1391 |
+
if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream.
|
| 1392 |
+
stdout_value = safe_decode(stdout_value)
|
| 1393 |
+
|
| 1394 |
+
# Allow access to the command's status code.
|
| 1395 |
+
if with_extended_output:
|
| 1396 |
+
return (status, stdout_value, safe_decode(stderr_value))
|
| 1397 |
+
else:
|
| 1398 |
+
return stdout_value
|
| 1399 |
+
|
| 1400 |
+
def environment(self) -> Dict[str, str]:
|
| 1401 |
+
return self._environment
|
| 1402 |
+
|
| 1403 |
+
def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
|
| 1404 |
+
"""Set environment variables for future git invocations. Return all changed
|
| 1405 |
+
values in a format that can be passed back into this function to revert the
|
| 1406 |
+
changes.
|
| 1407 |
+
|
| 1408 |
+
Examples::
|
| 1409 |
+
|
| 1410 |
+
old_env = self.update_environment(PWD='/tmp')
|
| 1411 |
+
self.update_environment(**old_env)
|
| 1412 |
+
|
| 1413 |
+
:param kwargs:
|
| 1414 |
+
Environment variables to use for git processes.
|
| 1415 |
+
|
| 1416 |
+
:return:
|
| 1417 |
+
Dict that maps environment variables to their old values
|
| 1418 |
+
"""
|
| 1419 |
+
old_env = {}
|
| 1420 |
+
for key, value in kwargs.items():
|
| 1421 |
+
# Set value if it is None.
|
| 1422 |
+
if value is not None:
|
| 1423 |
+
old_env[key] = self._environment.get(key)
|
| 1424 |
+
self._environment[key] = value
|
| 1425 |
+
# Remove key from environment if its value is None.
|
| 1426 |
+
elif key in self._environment:
|
| 1427 |
+
old_env[key] = self._environment[key]
|
| 1428 |
+
del self._environment[key]
|
| 1429 |
+
return old_env
|
| 1430 |
+
|
| 1431 |
+
@contextlib.contextmanager
|
| 1432 |
+
def custom_environment(self, **kwargs: Any) -> Iterator[None]:
|
| 1433 |
+
"""A context manager around the above :meth:`update_environment` method to
|
| 1434 |
+
restore the environment back to its previous state after operation.
|
| 1435 |
+
|
| 1436 |
+
Examples::
|
| 1437 |
+
|
| 1438 |
+
with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
|
| 1439 |
+
repo.remotes.origin.fetch()
|
| 1440 |
+
|
| 1441 |
+
:param kwargs:
|
| 1442 |
+
See :meth:`update_environment`.
|
| 1443 |
+
"""
|
| 1444 |
+
old_env = self.update_environment(**kwargs)
|
| 1445 |
+
try:
|
| 1446 |
+
yield
|
| 1447 |
+
finally:
|
| 1448 |
+
self.update_environment(**old_env)
|
| 1449 |
+
|
| 1450 |
+
def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]:
|
| 1451 |
+
if len(name) == 1:
|
| 1452 |
+
if value is True:
|
| 1453 |
+
return ["-%s" % name]
|
| 1454 |
+
elif value not in (False, None):
|
| 1455 |
+
if split_single_char_options:
|
| 1456 |
+
return ["-%s" % name, "%s" % value]
|
| 1457 |
+
else:
|
| 1458 |
+
return ["-%s%s" % (name, value)]
|
| 1459 |
+
else:
|
| 1460 |
+
if value is True:
|
| 1461 |
+
return ["--%s" % dashify(name)]
|
| 1462 |
+
elif value is not False and value is not None:
|
| 1463 |
+
return ["--%s=%s" % (dashify(name), value)]
|
| 1464 |
+
return []
|
| 1465 |
+
|
| 1466 |
+
def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
|
| 1467 |
+
"""Transform Python-style kwargs into git command line options."""
|
| 1468 |
+
args = []
|
| 1469 |
+
for k, v in kwargs.items():
|
| 1470 |
+
if isinstance(v, (list, tuple)):
|
| 1471 |
+
for value in v:
|
| 1472 |
+
args += self.transform_kwarg(k, value, split_single_char_options)
|
| 1473 |
+
else:
|
| 1474 |
+
args += self.transform_kwarg(k, v, split_single_char_options)
|
| 1475 |
+
return args
|
| 1476 |
+
|
| 1477 |
+
@classmethod
|
| 1478 |
+
def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
|
| 1479 |
+
outlist = []
|
| 1480 |
+
if isinstance(arg_list, (list, tuple)):
|
| 1481 |
+
for arg in arg_list:
|
| 1482 |
+
outlist.extend(cls._unpack_args(arg))
|
| 1483 |
+
else:
|
| 1484 |
+
outlist.append(str(arg_list))
|
| 1485 |
+
|
| 1486 |
+
return outlist
|
| 1487 |
+
|
| 1488 |
+
def __call__(self, **kwargs: Any) -> "Git":
|
| 1489 |
+
"""Specify command line options to the git executable for a subcommand call.
|
| 1490 |
+
|
| 1491 |
+
:param kwargs:
|
| 1492 |
+
A dict of keyword arguments.
|
| 1493 |
+
These arguments are passed as in :meth:`_call_process`, but will be passed
|
| 1494 |
+
to the git command rather than the subcommand.
|
| 1495 |
+
|
| 1496 |
+
Examples::
|
| 1497 |
+
|
| 1498 |
+
git(work_tree='/tmp').difftool()
|
| 1499 |
+
"""
|
| 1500 |
+
self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
|
| 1501 |
+
return self
|
| 1502 |
+
|
| 1503 |
+
@overload
|
| 1504 |
+
def _call_process(
|
| 1505 |
+
self, method: str, *args: None, **kwargs: None
|
| 1506 |
+
) -> str: ... # If no args were given, execute the call with all defaults.
|
| 1507 |
+
|
| 1508 |
+
@overload
|
| 1509 |
+
def _call_process(
|
| 1510 |
+
self,
|
| 1511 |
+
method: str,
|
| 1512 |
+
istream: int,
|
| 1513 |
+
as_process: Literal[True],
|
| 1514 |
+
*args: Any,
|
| 1515 |
+
**kwargs: Any,
|
| 1516 |
+
) -> "Git.AutoInterrupt": ...
|
| 1517 |
+
|
| 1518 |
+
@overload
|
| 1519 |
+
def _call_process(
|
| 1520 |
+
self, method: str, *args: Any, **kwargs: Any
|
| 1521 |
+
) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ...
|
| 1522 |
+
|
| 1523 |
+
def _call_process(
|
| 1524 |
+
self, method: str, *args: Any, **kwargs: Any
|
| 1525 |
+
) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
|
| 1526 |
+
"""Run the given git command with the specified arguments and return the result
|
| 1527 |
+
as a string.
|
| 1528 |
+
|
| 1529 |
+
:param method:
|
| 1530 |
+
The command. Contained ``_`` characters will be converted to hyphens, such
|
| 1531 |
+
as in ``ls_files`` to call ``ls-files``.
|
| 1532 |
+
|
| 1533 |
+
:param args:
|
| 1534 |
+
The list of arguments. If ``None`` is included, it will be pruned.
|
| 1535 |
+
This allows your commands to call git more conveniently, as ``None`` is
|
| 1536 |
+
realized as non-existent.
|
| 1537 |
+
|
| 1538 |
+
:param kwargs:
|
| 1539 |
+
Contains key-values for the following:
|
| 1540 |
+
|
| 1541 |
+
- The :meth:`execute()` kwds, as listed in ``execute_kwargs``.
|
| 1542 |
+
- "Command options" to be converted by :meth:`transform_kwargs`.
|
| 1543 |
+
- The ``insert_kwargs_after`` key which its value must match one of
|
| 1544 |
+
``*args``.
|
| 1545 |
+
|
| 1546 |
+
It also contains any command options, to be appended after the matched arg.
|
| 1547 |
+
|
| 1548 |
+
Examples::
|
| 1549 |
+
|
| 1550 |
+
git.rev_list('master', max_count=10, header=True)
|
| 1551 |
+
|
| 1552 |
+
turns into::
|
| 1553 |
+
|
| 1554 |
+
git rev-list max-count 10 --header master
|
| 1555 |
+
|
| 1556 |
+
:return:
|
| 1557 |
+
Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
|
| 1558 |
+
default (especially ``as_process = False``, ``stdout_as_string = True``) and
|
| 1559 |
+
return :class:`str`.
|
| 1560 |
+
"""
|
| 1561 |
+
# Handle optional arguments prior to calling transform_kwargs.
|
| 1562 |
+
# Otherwise these'll end up in args, which is bad.
|
| 1563 |
+
exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
|
| 1564 |
+
opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
|
| 1565 |
+
|
| 1566 |
+
insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None)
|
| 1567 |
+
|
| 1568 |
+
# Prepare the argument list.
|
| 1569 |
+
|
| 1570 |
+
opt_args = self.transform_kwargs(**opts_kwargs)
|
| 1571 |
+
ext_args = self._unpack_args([a for a in args if a is not None])
|
| 1572 |
+
|
| 1573 |
+
if insert_after_this_arg is None:
|
| 1574 |
+
args_list = opt_args + ext_args
|
| 1575 |
+
else:
|
| 1576 |
+
try:
|
| 1577 |
+
index = ext_args.index(insert_after_this_arg)
|
| 1578 |
+
except ValueError as err:
|
| 1579 |
+
raise ValueError(
|
| 1580 |
+
"Couldn't find argument '%s' in args %s to insert cmd options after"
|
| 1581 |
+
% (insert_after_this_arg, str(ext_args))
|
| 1582 |
+
) from err
|
| 1583 |
+
# END handle error
|
| 1584 |
+
args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :]
|
| 1585 |
+
# END handle opts_kwargs
|
| 1586 |
+
|
| 1587 |
+
call = [self.GIT_PYTHON_GIT_EXECUTABLE]
|
| 1588 |
+
|
| 1589 |
+
# Add persistent git options.
|
| 1590 |
+
call.extend(self._persistent_git_options)
|
| 1591 |
+
|
| 1592 |
+
# Add the git options, then reset to empty to avoid side effects.
|
| 1593 |
+
call.extend(self._git_options)
|
| 1594 |
+
self._git_options = ()
|
| 1595 |
+
|
| 1596 |
+
call.append(dashify(method))
|
| 1597 |
+
call.extend(args_list)
|
| 1598 |
+
|
| 1599 |
+
return self.execute(call, **exec_kwargs)
|
| 1600 |
+
|
| 1601 |
+
def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
|
| 1602 |
+
"""
|
| 1603 |
+
:param header_line:
|
| 1604 |
+
A line of the form::
|
| 1605 |
+
|
| 1606 |
+
<hex_sha> type_string size_as_int
|
| 1607 |
+
|
| 1608 |
+
:return:
|
| 1609 |
+
(hex_sha, type_string, size_as_int)
|
| 1610 |
+
|
| 1611 |
+
:raise ValueError:
|
| 1612 |
+
If the header contains indication for an error due to incorrect input sha.
|
| 1613 |
+
"""
|
| 1614 |
+
tokens = header_line.split()
|
| 1615 |
+
if len(tokens) != 3:
|
| 1616 |
+
if not tokens:
|
| 1617 |
+
err_msg = (
|
| 1618 |
+
f"SHA is empty, possible dubious ownership in the repository "
|
| 1619 |
+
f"""at {self._working_dir}.\n If this is unintended run:\n\n """
|
| 1620 |
+
f""" "git config --global --add safe.directory {self._working_dir}" """
|
| 1621 |
+
)
|
| 1622 |
+
raise ValueError(err_msg)
|
| 1623 |
+
else:
|
| 1624 |
+
raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip()))
|
| 1625 |
+
# END handle actual return value
|
| 1626 |
+
# END error handling
|
| 1627 |
+
|
| 1628 |
+
if len(tokens[0]) != 40:
|
| 1629 |
+
raise ValueError("Failed to parse header: %r" % header_line)
|
| 1630 |
+
return (tokens[0], tokens[1], int(tokens[2]))
|
| 1631 |
+
|
| 1632 |
+
def _prepare_ref(self, ref: AnyStr) -> bytes:
|
| 1633 |
+
# Required for command to separate refs on stdin, as bytes.
|
| 1634 |
+
if isinstance(ref, bytes):
|
| 1635 |
+
# Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text.
|
| 1636 |
+
refstr: str = ref.decode("ascii")
|
| 1637 |
+
elif not isinstance(ref, str):
|
| 1638 |
+
refstr = str(ref) # Could be ref-object.
|
| 1639 |
+
else:
|
| 1640 |
+
refstr = ref
|
| 1641 |
+
|
| 1642 |
+
if not refstr.endswith("\n"):
|
| 1643 |
+
refstr += "\n"
|
| 1644 |
+
return refstr.encode(defenc)
|
| 1645 |
+
|
| 1646 |
+
def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt":
|
| 1647 |
+
cur_val = getattr(self, attr_name)
|
| 1648 |
+
if cur_val is not None:
|
| 1649 |
+
return cur_val
|
| 1650 |
+
|
| 1651 |
+
options = {"istream": PIPE, "as_process": True}
|
| 1652 |
+
options.update(kwargs)
|
| 1653 |
+
|
| 1654 |
+
cmd = self._call_process(cmd_name, *args, **options)
|
| 1655 |
+
setattr(self, attr_name, cmd)
|
| 1656 |
+
cmd = cast("Git.AutoInterrupt", cmd)
|
| 1657 |
+
return cmd
|
| 1658 |
+
|
| 1659 |
+
def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]:
|
| 1660 |
+
if cmd.stdin and cmd.stdout:
|
| 1661 |
+
cmd.stdin.write(self._prepare_ref(ref))
|
| 1662 |
+
cmd.stdin.flush()
|
| 1663 |
+
return self._parse_object_header(cmd.stdout.readline())
|
| 1664 |
+
else:
|
| 1665 |
+
raise ValueError("cmd stdin was empty")
|
| 1666 |
+
|
| 1667 |
+
def get_object_header(self, ref: str) -> Tuple[str, str, int]:
|
| 1668 |
+
"""Use this method to quickly examine the type and size of the object behind the
|
| 1669 |
+
given ref.
|
| 1670 |
+
|
| 1671 |
+
:note:
|
| 1672 |
+
The method will only suffer from the costs of command invocation once and
|
| 1673 |
+
reuses the command in subsequent calls.
|
| 1674 |
+
|
| 1675 |
+
:return:
|
| 1676 |
+
(hexsha, type_string, size_as_int)
|
| 1677 |
+
"""
|
| 1678 |
+
cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
|
| 1679 |
+
return self.__get_object_header(cmd, ref)
|
| 1680 |
+
|
| 1681 |
+
def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
|
| 1682 |
+
"""Similar to :meth:`get_object_header`, but returns object data as well.
|
| 1683 |
+
|
| 1684 |
+
:return:
|
| 1685 |
+
(hexsha, type_string, size_as_int, data_string)
|
| 1686 |
+
|
| 1687 |
+
:note:
|
| 1688 |
+
Not threadsafe.
|
| 1689 |
+
"""
|
| 1690 |
+
hexsha, typename, size, stream = self.stream_object_data(ref)
|
| 1691 |
+
data = stream.read(size)
|
| 1692 |
+
del stream
|
| 1693 |
+
return (hexsha, typename, size, data)
|
| 1694 |
+
|
| 1695 |
+
def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
|
| 1696 |
+
"""Similar to :meth:`get_object_data`, but returns the data as a stream.
|
| 1697 |
+
|
| 1698 |
+
:return:
|
| 1699 |
+
(hexsha, type_string, size_as_int, stream)
|
| 1700 |
+
|
| 1701 |
+
:note:
|
| 1702 |
+
This method is not threadsafe. You need one independent :class:`Git`
|
| 1703 |
+
instance per thread to be safe!
|
| 1704 |
+
"""
|
| 1705 |
+
cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
|
| 1706 |
+
hexsha, typename, size = self.__get_object_header(cmd, ref)
|
| 1707 |
+
cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()
|
| 1708 |
+
return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout))
|
| 1709 |
+
|
| 1710 |
+
def clear_cache(self) -> "Git":
|
| 1711 |
+
"""Clear all kinds of internal caches to release resources.
|
| 1712 |
+
|
| 1713 |
+
Currently persistent commands will be interrupted.
|
| 1714 |
+
|
| 1715 |
+
:return:
|
| 1716 |
+
self
|
| 1717 |
+
"""
|
| 1718 |
+
for cmd in (self.cat_file_all, self.cat_file_header):
|
| 1719 |
+
if cmd:
|
| 1720 |
+
cmd.__del__()
|
| 1721 |
+
|
| 1722 |
+
self.cat_file_all = None
|
| 1723 |
+
self.cat_file_header = None
|
| 1724 |
+
return self
|
venv/lib/python3.10/site-packages/git/compat.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
"""Utilities to help provide compatibility with Python 3.
|
| 7 |
+
|
| 8 |
+
This module exists for historical reasons. Code outside GitPython may make use of public
|
| 9 |
+
members of this module, but is unlikely to benefit from doing so. GitPython continues to
|
| 10 |
+
use some of these utilities, in some cases for compatibility across different platforms.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import locale
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import warnings
|
| 17 |
+
|
| 18 |
+
from gitdb.utils.encoding import force_bytes, force_text # noqa: F401
|
| 19 |
+
|
| 20 |
+
# typing --------------------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
from typing import (
|
| 23 |
+
Any, # noqa: F401
|
| 24 |
+
AnyStr,
|
| 25 |
+
Dict, # noqa: F401
|
| 26 |
+
IO, # noqa: F401
|
| 27 |
+
List,
|
| 28 |
+
Optional,
|
| 29 |
+
TYPE_CHECKING,
|
| 30 |
+
Tuple, # noqa: F401
|
| 31 |
+
Type, # noqa: F401
|
| 32 |
+
Union,
|
| 33 |
+
overload,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
_deprecated_platform_aliases = {
|
| 40 |
+
"is_win": os.name == "nt",
|
| 41 |
+
"is_posix": os.name == "posix",
|
| 42 |
+
"is_darwin": sys.platform == "darwin",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _getattr(name: str) -> Any:
|
| 47 |
+
try:
|
| 48 |
+
value = _deprecated_platform_aliases[name]
|
| 49 |
+
except KeyError:
|
| 50 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
| 51 |
+
|
| 52 |
+
warnings.warn(
|
| 53 |
+
f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
|
| 54 |
+
"Write the desired os.name or sys.platform check explicitly instead.",
|
| 55 |
+
DeprecationWarning,
|
| 56 |
+
stacklevel=2,
|
| 57 |
+
)
|
| 58 |
+
return value
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
|
| 62 |
+
__getattr__ = _getattr
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def __dir__() -> List[str]:
|
| 66 |
+
return [*globals(), *_deprecated_platform_aliases]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
is_win: bool
|
| 70 |
+
"""Deprecated alias for ``os.name == "nt"`` to check for native Windows.
|
| 71 |
+
|
| 72 |
+
This is deprecated because it is clearer to write out :attr:`os.name` or
|
| 73 |
+
:attr:`sys.platform` checks explicitly, especially in cases where it matters which is
|
| 74 |
+
used.
|
| 75 |
+
|
| 76 |
+
:note:
|
| 77 |
+
``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To detect
|
| 78 |
+
Cygwin, use ``sys.platform == "cygwin"``.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
is_posix: bool
|
| 82 |
+
"""Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
|
| 83 |
+
|
| 84 |
+
This is deprecated because it clearer to write out :attr:`os.name` or
|
| 85 |
+
:attr:`sys.platform` checks explicitly, especially in cases where it matters which is
|
| 86 |
+
used.
|
| 87 |
+
|
| 88 |
+
:note:
|
| 89 |
+
For POSIX systems, more detailed information is available in :attr:`sys.platform`,
|
| 90 |
+
while :attr:`os.name` is always ``"posix"`` on such systems, including macOS
|
| 91 |
+
(Darwin).
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
is_darwin: bool
|
| 95 |
+
"""Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
|
| 96 |
+
|
| 97 |
+
This is deprecated because it clearer to write out :attr:`os.name` or
|
| 98 |
+
:attr:`sys.platform` checks explicitly.
|
| 99 |
+
|
| 100 |
+
:note:
|
| 101 |
+
For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
|
| 102 |
+
``sys.platform == "darwin"``.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
defenc = sys.getfilesystemencoding()
|
| 106 |
+
"""The encoding used to convert between Unicode and bytes filenames."""
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@overload
|
| 110 |
+
def safe_decode(s: None) -> None: ...
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@overload
|
| 114 |
+
def safe_decode(s: AnyStr) -> str: ...
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def safe_decode(s: Union[AnyStr, None]) -> Optional[str]:
|
| 118 |
+
"""Safely decode a binary string to Unicode."""
|
| 119 |
+
if isinstance(s, str):
|
| 120 |
+
return s
|
| 121 |
+
elif isinstance(s, bytes):
|
| 122 |
+
return s.decode(defenc, "surrogateescape")
|
| 123 |
+
elif s is None:
|
| 124 |
+
return None
|
| 125 |
+
else:
|
| 126 |
+
raise TypeError("Expected bytes or text, but got %r" % (s,))
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@overload
|
| 130 |
+
def safe_encode(s: None) -> None: ...
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@overload
|
| 134 |
+
def safe_encode(s: AnyStr) -> bytes: ...
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]:
|
| 138 |
+
"""Safely encode a binary string to Unicode."""
|
| 139 |
+
if isinstance(s, str):
|
| 140 |
+
return s.encode(defenc)
|
| 141 |
+
elif isinstance(s, bytes):
|
| 142 |
+
return s
|
| 143 |
+
elif s is None:
|
| 144 |
+
return None
|
| 145 |
+
else:
|
| 146 |
+
raise TypeError("Expected bytes or text, but got %r" % (s,))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@overload
|
| 150 |
+
def win_encode(s: None) -> None: ...
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@overload
|
| 154 |
+
def win_encode(s: AnyStr) -> bytes: ...
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def win_encode(s: Optional[AnyStr]) -> Optional[bytes]:
|
| 158 |
+
"""Encode Unicode strings for process arguments on Windows."""
|
| 159 |
+
if isinstance(s, str):
|
| 160 |
+
return s.encode(locale.getpreferredencoding(False))
|
| 161 |
+
elif isinstance(s, bytes):
|
| 162 |
+
return s
|
| 163 |
+
elif s is not None:
|
| 164 |
+
raise TypeError("Expected bytes or text, but got %r" % (s,))
|
| 165 |
+
return None
|
venv/lib/python3.10/site-packages/git/config.py
ADDED
|
@@ -0,0 +1,944 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
"""Parser for reading and writing configuration files."""
|
| 7 |
+
|
| 8 |
+
__all__ = ["GitConfigParser", "SectionConstraint"]
|
| 9 |
+
|
| 10 |
+
import abc
|
| 11 |
+
import configparser as cp
|
| 12 |
+
import fnmatch
|
| 13 |
+
from functools import wraps
|
| 14 |
+
import inspect
|
| 15 |
+
from io import BufferedReader, IOBase
|
| 16 |
+
import logging
|
| 17 |
+
import os
|
| 18 |
+
import os.path as osp
|
| 19 |
+
import re
|
| 20 |
+
import sys
|
| 21 |
+
|
| 22 |
+
from git.compat import defenc, force_text
|
| 23 |
+
from git.util import LockFile
|
| 24 |
+
|
| 25 |
+
# typing-------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
from typing import (
|
| 28 |
+
Any,
|
| 29 |
+
Callable,
|
| 30 |
+
Generic,
|
| 31 |
+
IO,
|
| 32 |
+
List,
|
| 33 |
+
Dict,
|
| 34 |
+
Sequence,
|
| 35 |
+
TYPE_CHECKING,
|
| 36 |
+
Tuple,
|
| 37 |
+
TypeVar,
|
| 38 |
+
Union,
|
| 39 |
+
cast,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T
|
| 43 |
+
|
| 44 |
+
if TYPE_CHECKING:
|
| 45 |
+
from io import BytesIO
|
| 46 |
+
|
| 47 |
+
from git.repo.base import Repo
|
| 48 |
+
|
| 49 |
+
T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser")
|
| 50 |
+
T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool)
|
| 51 |
+
|
| 52 |
+
if sys.version_info[:3] < (3, 7, 2):
|
| 53 |
+
# typing.Ordereddict not added until Python 3.7.2.
|
| 54 |
+
from collections import OrderedDict
|
| 55 |
+
|
| 56 |
+
OrderedDict_OMD = OrderedDict
|
| 57 |
+
else:
|
| 58 |
+
from typing import OrderedDict
|
| 59 |
+
|
| 60 |
+
OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc]
|
| 61 |
+
|
| 62 |
+
# -------------------------------------------------------------
|
| 63 |
+
|
| 64 |
+
_logger = logging.getLogger(__name__)
|
| 65 |
+
|
| 66 |
+
CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
|
| 67 |
+
"""The configuration level of a configuration file."""
|
| 68 |
+
|
| 69 |
+
CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
|
| 70 |
+
"""Section pattern to detect conditional includes.
|
| 71 |
+
|
| 72 |
+
See: https://git-scm.com/docs/git-config#_conditional_includes
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class MetaParserBuilder(abc.ABCMeta): # noqa: B024
|
| 77 |
+
"""Utility class wrapping base-class methods into decorators that assure read-only
|
| 78 |
+
properties."""
|
| 79 |
+
|
| 80 |
+
def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder":
|
| 81 |
+
"""Equip all base-class methods with a needs_values decorator, and all non-const
|
| 82 |
+
methods with a :func:`set_dirty_and_flush_changes` decorator in addition to
|
| 83 |
+
that.
|
| 84 |
+
"""
|
| 85 |
+
kmm = "_mutating_methods_"
|
| 86 |
+
if kmm in clsdict:
|
| 87 |
+
mutating_methods = clsdict[kmm]
|
| 88 |
+
for base in bases:
|
| 89 |
+
methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_"))
|
| 90 |
+
for name, method in methods:
|
| 91 |
+
if name in clsdict:
|
| 92 |
+
continue
|
| 93 |
+
method_with_values = needs_values(method)
|
| 94 |
+
if name in mutating_methods:
|
| 95 |
+
method_with_values = set_dirty_and_flush_changes(method_with_values)
|
| 96 |
+
# END mutating methods handling
|
| 97 |
+
|
| 98 |
+
clsdict[name] = method_with_values
|
| 99 |
+
# END for each name/method pair
|
| 100 |
+
# END for each base
|
| 101 |
+
# END if mutating methods configuration is set
|
| 102 |
+
|
| 103 |
+
new_type = super().__new__(cls, name, bases, clsdict)
|
| 104 |
+
return new_type
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def needs_values(func: Callable[..., _T]) -> Callable[..., _T]:
|
| 108 |
+
"""Return a method for ensuring we read values (on demand) before we try to access
|
| 109 |
+
them."""
|
| 110 |
+
|
| 111 |
+
@wraps(func)
|
| 112 |
+
def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
|
| 113 |
+
self.read()
|
| 114 |
+
return func(self, *args, **kwargs)
|
| 115 |
+
|
| 116 |
+
# END wrapper method
|
| 117 |
+
return assure_data_present
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]:
|
| 121 |
+
"""Return a method that checks whether given non constant function may be called.
|
| 122 |
+
|
| 123 |
+
If so, the instance will be set dirty. Additionally, we flush the changes right to
|
| 124 |
+
disk.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
|
| 128 |
+
rval = non_const_func(self, *args, **kwargs)
|
| 129 |
+
self._dirty = True
|
| 130 |
+
self.write()
|
| 131 |
+
return rval
|
| 132 |
+
|
| 133 |
+
# END wrapper method
|
| 134 |
+
flush_changes.__name__ = non_const_func.__name__
|
| 135 |
+
return flush_changes
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class SectionConstraint(Generic[T_ConfigParser]):
|
| 139 |
+
"""Constrains a ConfigParser to only option commands which are constrained to
|
| 140 |
+
always use the section we have been initialized with.
|
| 141 |
+
|
| 142 |
+
It supports all ConfigParser methods that operate on an option.
|
| 143 |
+
|
| 144 |
+
:note:
|
| 145 |
+
If used as a context manager, will release the wrapped ConfigParser.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
__slots__ = ("_config", "_section_name")
|
| 149 |
+
|
| 150 |
+
_valid_attrs_ = (
|
| 151 |
+
"get_value",
|
| 152 |
+
"set_value",
|
| 153 |
+
"get",
|
| 154 |
+
"set",
|
| 155 |
+
"getint",
|
| 156 |
+
"getfloat",
|
| 157 |
+
"getboolean",
|
| 158 |
+
"has_option",
|
| 159 |
+
"remove_section",
|
| 160 |
+
"remove_option",
|
| 161 |
+
"options",
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def __init__(self, config: T_ConfigParser, section: str) -> None:
|
| 165 |
+
self._config = config
|
| 166 |
+
self._section_name = section
|
| 167 |
+
|
| 168 |
+
def __del__(self) -> None:
|
| 169 |
+
# Yes, for some reason, we have to call it explicitly for it to work in PY3 !
|
| 170 |
+
# Apparently __del__ doesn't get call anymore if refcount becomes 0
|
| 171 |
+
# Ridiculous ... .
|
| 172 |
+
self._config.release()
|
| 173 |
+
|
| 174 |
+
def __getattr__(self, attr: str) -> Any:
|
| 175 |
+
if attr in self._valid_attrs_:
|
| 176 |
+
return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs)
|
| 177 |
+
return super().__getattribute__(attr)
|
| 178 |
+
|
| 179 |
+
def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any:
|
| 180 |
+
"""Call the configuration at the given method which must take a section name as
|
| 181 |
+
first argument."""
|
| 182 |
+
return getattr(self._config, method)(self._section_name, *args, **kwargs)
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def config(self) -> T_ConfigParser:
|
| 186 |
+
"""return: ConfigParser instance we constrain"""
|
| 187 |
+
return self._config
|
| 188 |
+
|
| 189 |
+
def release(self) -> None:
|
| 190 |
+
"""Equivalent to :meth:`GitConfigParser.release`, which is called on our
|
| 191 |
+
underlying parser instance."""
|
| 192 |
+
return self._config.release()
|
| 193 |
+
|
| 194 |
+
def __enter__(self) -> "SectionConstraint[T_ConfigParser]":
|
| 195 |
+
self._config.__enter__()
|
| 196 |
+
return self
|
| 197 |
+
|
| 198 |
+
def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None:
|
| 199 |
+
self._config.__exit__(exception_type, exception_value, traceback)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class _OMD(OrderedDict_OMD):
|
| 203 |
+
"""Ordered multi-dict."""
|
| 204 |
+
|
| 205 |
+
def __setitem__(self, key: str, value: _T) -> None:
|
| 206 |
+
super().__setitem__(key, [value])
|
| 207 |
+
|
| 208 |
+
def add(self, key: str, value: Any) -> None:
|
| 209 |
+
if key not in self:
|
| 210 |
+
super().__setitem__(key, [value])
|
| 211 |
+
return
|
| 212 |
+
|
| 213 |
+
super().__getitem__(key).append(value)
|
| 214 |
+
|
| 215 |
+
def setall(self, key: str, values: List[_T]) -> None:
|
| 216 |
+
super().__setitem__(key, values)
|
| 217 |
+
|
| 218 |
+
def __getitem__(self, key: str) -> Any:
|
| 219 |
+
return super().__getitem__(key)[-1]
|
| 220 |
+
|
| 221 |
+
def getlast(self, key: str) -> Any:
|
| 222 |
+
return super().__getitem__(key)[-1]
|
| 223 |
+
|
| 224 |
+
def setlast(self, key: str, value: Any) -> None:
|
| 225 |
+
if key not in self:
|
| 226 |
+
super().__setitem__(key, [value])
|
| 227 |
+
return
|
| 228 |
+
|
| 229 |
+
prior = super().__getitem__(key)
|
| 230 |
+
prior[-1] = value
|
| 231 |
+
|
| 232 |
+
def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]:
|
| 233 |
+
return super().get(key, [default])[-1]
|
| 234 |
+
|
| 235 |
+
def getall(self, key: str) -> List[_T]:
|
| 236 |
+
return super().__getitem__(key)
|
| 237 |
+
|
| 238 |
+
def items(self) -> List[Tuple[str, _T]]: # type: ignore[override]
|
| 239 |
+
"""List of (key, last value for key)."""
|
| 240 |
+
return [(k, self[k]) for k in self]
|
| 241 |
+
|
| 242 |
+
def items_all(self) -> List[Tuple[str, List[_T]]]:
|
| 243 |
+
"""List of (key, list of values for key)."""
|
| 244 |
+
return [(k, self.getall(k)) for k in self]
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_config_path(config_level: Lit_config_levels) -> str:
|
| 248 |
+
# We do not support an absolute path of the gitconfig on Windows.
|
| 249 |
+
# Use the global config instead.
|
| 250 |
+
if sys.platform == "win32" and config_level == "system":
|
| 251 |
+
config_level = "global"
|
| 252 |
+
|
| 253 |
+
if config_level == "system":
|
| 254 |
+
return "/etc/gitconfig"
|
| 255 |
+
elif config_level == "user":
|
| 256 |
+
config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
|
| 257 |
+
return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
|
| 258 |
+
elif config_level == "global":
|
| 259 |
+
return osp.normpath(osp.expanduser("~/.gitconfig"))
|
| 260 |
+
elif config_level == "repository":
|
| 261 |
+
raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path")
|
| 262 |
+
else:
|
| 263 |
+
# Should not reach here. Will raise ValueError if does. Static typing will warn
|
| 264 |
+
# about missing elifs.
|
| 265 |
+
assert_never( # type: ignore[unreachable]
|
| 266 |
+
config_level,
|
| 267 |
+
ValueError(f"Invalid configuration level: {config_level!r}"),
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
| 272 |
+
"""Implements specifics required to read git style configuration files.
|
| 273 |
+
|
| 274 |
+
This variation behaves much like the :manpage:`git-config(1)` command, such that the
|
| 275 |
+
configuration will be read on demand based on the filepath given during
|
| 276 |
+
initialization.
|
| 277 |
+
|
| 278 |
+
The changes will automatically be written once the instance goes out of scope, but
|
| 279 |
+
can be triggered manually as well.
|
| 280 |
+
|
| 281 |
+
The configuration file will be locked if you intend to change values preventing
|
| 282 |
+
other instances to write concurrently.
|
| 283 |
+
|
| 284 |
+
:note:
|
| 285 |
+
The config is case-sensitive even when queried, hence section and option names
|
| 286 |
+
must match perfectly.
|
| 287 |
+
|
| 288 |
+
:note:
|
| 289 |
+
If used as a context manager, this will release the locked file.
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
+
# { Configuration
|
| 293 |
+
t_lock = LockFile
|
| 294 |
+
"""The lock type determines the type of lock to use in new configuration readers.
|
| 295 |
+
|
| 296 |
+
They must be compatible to the :class:`~git.util.LockFile` interface.
|
| 297 |
+
A suitable alternative would be the :class:`~git.util.BlockingLockFile`.
|
| 298 |
+
"""
|
| 299 |
+
|
| 300 |
+
re_comment = re.compile(r"^\s*[#;]")
|
| 301 |
+
# } END configuration
|
| 302 |
+
|
| 303 |
+
optvalueonly_source = r"\s*(?P<option>[^:=\s][^:=]*)"
|
| 304 |
+
|
| 305 |
+
OPTVALUEONLY = re.compile(optvalueonly_source)
|
| 306 |
+
|
| 307 |
+
OPTCRE = re.compile(optvalueonly_source + r"\s*(?P<vi>[:=])\s*" + r"(?P<value>.*)$")
|
| 308 |
+
|
| 309 |
+
del optvalueonly_source
|
| 310 |
+
|
| 311 |
+
_mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
|
| 312 |
+
"""Names of :class:`~configparser.RawConfigParser` methods able to change the
|
| 313 |
+
instance."""
|
| 314 |
+
|
| 315 |
+
def __init__(
|
| 316 |
+
self,
|
| 317 |
+
file_or_files: Union[None, PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = None,
|
| 318 |
+
read_only: bool = True,
|
| 319 |
+
merge_includes: bool = True,
|
| 320 |
+
config_level: Union[Lit_config_levels, None] = None,
|
| 321 |
+
repo: Union["Repo", None] = None,
|
| 322 |
+
) -> None:
|
| 323 |
+
"""Initialize a configuration reader to read the given `file_or_files` and to
|
| 324 |
+
possibly allow changes to it by setting `read_only` False.
|
| 325 |
+
|
| 326 |
+
:param file_or_files:
|
| 327 |
+
A file path or file object, or a sequence of possibly more than one of them.
|
| 328 |
+
|
| 329 |
+
:param read_only:
|
| 330 |
+
If ``True``, the ConfigParser may only read the data, but not change it.
|
| 331 |
+
If ``False``, only a single file path or file object may be given. We will
|
| 332 |
+
write back the changes when they happen, or when the ConfigParser is
|
| 333 |
+
released. This will not happen if other configuration files have been
|
| 334 |
+
included.
|
| 335 |
+
|
| 336 |
+
:param merge_includes:
|
| 337 |
+
If ``True``, we will read files mentioned in ``[include]`` sections and
|
| 338 |
+
merge their contents into ours. This makes it impossible to write back an
|
| 339 |
+
individual configuration file. Thus, if you want to modify a single
|
| 340 |
+
configuration file, turn this off to leave the original dataset unaltered
|
| 341 |
+
when reading it.
|
| 342 |
+
|
| 343 |
+
:param repo:
|
| 344 |
+
Reference to repository to use if ``[includeIf]`` sections are found in
|
| 345 |
+
configuration files.
|
| 346 |
+
"""
|
| 347 |
+
cp.RawConfigParser.__init__(self, dict_type=_OMD)
|
| 348 |
+
self._dict: Callable[..., _OMD]
|
| 349 |
+
self._defaults: _OMD
|
| 350 |
+
self._sections: _OMD
|
| 351 |
+
|
| 352 |
+
# Used in Python 3. Needs to stay in sync with sections for underlying
|
| 353 |
+
# implementation to work.
|
| 354 |
+
if not hasattr(self, "_proxies"):
|
| 355 |
+
self._proxies = self._dict()
|
| 356 |
+
|
| 357 |
+
if file_or_files is not None:
|
| 358 |
+
self._file_or_files: Union[PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = file_or_files
|
| 359 |
+
else:
|
| 360 |
+
if config_level is None:
|
| 361 |
+
if read_only:
|
| 362 |
+
self._file_or_files = [
|
| 363 |
+
get_config_path(cast(Lit_config_levels, f)) for f in CONFIG_LEVELS if f != "repository"
|
| 364 |
+
]
|
| 365 |
+
else:
|
| 366 |
+
raise ValueError("No configuration level or configuration files specified")
|
| 367 |
+
else:
|
| 368 |
+
self._file_or_files = [get_config_path(config_level)]
|
| 369 |
+
|
| 370 |
+
self._read_only = read_only
|
| 371 |
+
self._dirty = False
|
| 372 |
+
self._is_initialized = False
|
| 373 |
+
self._merge_includes = merge_includes
|
| 374 |
+
self._repo = repo
|
| 375 |
+
self._lock: Union["LockFile", None] = None
|
| 376 |
+
self._acquire_lock()
|
| 377 |
+
|
| 378 |
+
def _acquire_lock(self) -> None:
|
| 379 |
+
if not self._read_only:
|
| 380 |
+
if not self._lock:
|
| 381 |
+
if isinstance(self._file_or_files, (str, os.PathLike)):
|
| 382 |
+
file_or_files = self._file_or_files
|
| 383 |
+
elif isinstance(self._file_or_files, (tuple, list, Sequence)):
|
| 384 |
+
raise ValueError(
|
| 385 |
+
"Write-ConfigParsers can operate on a single file only, multiple files have been passed"
|
| 386 |
+
)
|
| 387 |
+
else:
|
| 388 |
+
file_or_files = self._file_or_files.name
|
| 389 |
+
|
| 390 |
+
# END get filename from handle/stream
|
| 391 |
+
# Initialize lock base - we want to write.
|
| 392 |
+
self._lock = self.t_lock(file_or_files)
|
| 393 |
+
# END lock check
|
| 394 |
+
|
| 395 |
+
self._lock._obtain_lock()
|
| 396 |
+
# END read-only check
|
| 397 |
+
|
| 398 |
+
def __del__(self) -> None:
|
| 399 |
+
"""Write pending changes if required and release locks."""
|
| 400 |
+
# NOTE: Only consistent in Python 2.
|
| 401 |
+
self.release()
|
| 402 |
+
|
| 403 |
+
def __enter__(self) -> "GitConfigParser":
|
| 404 |
+
self._acquire_lock()
|
| 405 |
+
return self
|
| 406 |
+
|
| 407 |
+
def __exit__(self, *args: Any) -> None:
|
| 408 |
+
self.release()
|
| 409 |
+
|
| 410 |
+
def release(self) -> None:
|
| 411 |
+
"""Flush changes and release the configuration write lock. This instance must
|
| 412 |
+
not be used anymore afterwards.
|
| 413 |
+
|
| 414 |
+
In Python 3, it's required to explicitly release locks and flush changes, as
|
| 415 |
+
``__del__`` is not called deterministically anymore.
|
| 416 |
+
"""
|
| 417 |
+
# Checking for the lock here makes sure we do not raise during write()
|
| 418 |
+
# in case an invalid parser was created who could not get a lock.
|
| 419 |
+
if self.read_only or (self._lock and not self._lock._has_lock()):
|
| 420 |
+
return
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
self.write()
|
| 424 |
+
except IOError:
|
| 425 |
+
_logger.error("Exception during destruction of GitConfigParser", exc_info=True)
|
| 426 |
+
except ReferenceError:
|
| 427 |
+
# This happens in Python 3... and usually means that some state cannot be
|
| 428 |
+
# written as the sections dict cannot be iterated. This usually happens when
|
| 429 |
+
# the interpreter is shutting down. Can it be fixed?
|
| 430 |
+
pass
|
| 431 |
+
finally:
|
| 432 |
+
if self._lock is not None:
|
| 433 |
+
self._lock._release_lock()
|
| 434 |
+
|
| 435 |
+
def optionxform(self, optionstr: str) -> str:
|
| 436 |
+
"""Do not transform options in any way when writing."""
|
| 437 |
+
return optionstr
|
| 438 |
+
|
| 439 |
+
def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
|
| 440 |
+
"""Originally a direct copy of the Python 2.4 version of
|
| 441 |
+
:meth:`RawConfigParser._read <configparser.RawConfigParser._read>`, to ensure it
|
| 442 |
+
uses ordered dicts.
|
| 443 |
+
|
| 444 |
+
The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since
|
| 445 |
+
Python 3.7. This has some other changes, especially that it ignores initial
|
| 446 |
+
whitespace, since git uses tabs. (Big comments are removed to be more compact.)
|
| 447 |
+
"""
|
| 448 |
+
cursect = None # None, or a dictionary.
|
| 449 |
+
optname = None
|
| 450 |
+
lineno = 0
|
| 451 |
+
is_multi_line = False
|
| 452 |
+
e = None # None, or an exception.
|
| 453 |
+
|
| 454 |
+
def string_decode(v: str) -> str:
|
| 455 |
+
if v and v.endswith("\\"):
|
| 456 |
+
v = v[:-1]
|
| 457 |
+
# END cut trailing escapes to prevent decode error
|
| 458 |
+
|
| 459 |
+
return v.encode(defenc).decode("unicode_escape")
|
| 460 |
+
|
| 461 |
+
# END string_decode
|
| 462 |
+
|
| 463 |
+
while True:
|
| 464 |
+
# We assume to read binary!
|
| 465 |
+
line = fp.readline().decode(defenc)
|
| 466 |
+
if not line:
|
| 467 |
+
break
|
| 468 |
+
lineno = lineno + 1
|
| 469 |
+
# Comment or blank line?
|
| 470 |
+
if line.strip() == "" or self.re_comment.match(line):
|
| 471 |
+
continue
|
| 472 |
+
if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR":
|
| 473 |
+
# No leading whitespace.
|
| 474 |
+
continue
|
| 475 |
+
|
| 476 |
+
# Is it a section header?
|
| 477 |
+
mo = self.SECTCRE.match(line.strip())
|
| 478 |
+
if not is_multi_line and mo:
|
| 479 |
+
sectname: str = mo.group("header").strip()
|
| 480 |
+
if sectname in self._sections:
|
| 481 |
+
cursect = self._sections[sectname]
|
| 482 |
+
elif sectname == cp.DEFAULTSECT:
|
| 483 |
+
cursect = self._defaults
|
| 484 |
+
else:
|
| 485 |
+
cursect = self._dict((("__name__", sectname),))
|
| 486 |
+
self._sections[sectname] = cursect
|
| 487 |
+
self._proxies[sectname] = None
|
| 488 |
+
# So sections can't start with a continuation line.
|
| 489 |
+
optname = None
|
| 490 |
+
# No section header in the file?
|
| 491 |
+
elif cursect is None:
|
| 492 |
+
raise cp.MissingSectionHeaderError(fpname, lineno, line)
|
| 493 |
+
# An option line?
|
| 494 |
+
elif not is_multi_line:
|
| 495 |
+
mo = self.OPTCRE.match(line)
|
| 496 |
+
if mo:
|
| 497 |
+
# We might just have handled the last line, which could contain a quotation we want to remove.
|
| 498 |
+
optname, vi, optval = mo.group("option", "vi", "value")
|
| 499 |
+
if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
|
| 500 |
+
pos = optval.find(";")
|
| 501 |
+
if pos != -1 and optval[pos - 1].isspace():
|
| 502 |
+
optval = optval[:pos]
|
| 503 |
+
optval = optval.strip()
|
| 504 |
+
if optval == '""':
|
| 505 |
+
optval = ""
|
| 506 |
+
# END handle empty string
|
| 507 |
+
optname = self.optionxform(optname.rstrip())
|
| 508 |
+
if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
|
| 509 |
+
is_multi_line = True
|
| 510 |
+
optval = string_decode(optval[1:])
|
| 511 |
+
# END handle multi-line
|
| 512 |
+
# Preserves multiple values for duplicate optnames.
|
| 513 |
+
cursect.add(optname, optval)
|
| 514 |
+
else:
|
| 515 |
+
# Check if it's an option with no value - it's just ignored by git.
|
| 516 |
+
if not self.OPTVALUEONLY.match(line):
|
| 517 |
+
if not e:
|
| 518 |
+
e = cp.ParsingError(fpname)
|
| 519 |
+
e.append(lineno, repr(line))
|
| 520 |
+
continue
|
| 521 |
+
else:
|
| 522 |
+
line = line.rstrip()
|
| 523 |
+
if line.endswith('"'):
|
| 524 |
+
is_multi_line = False
|
| 525 |
+
line = line[:-1]
|
| 526 |
+
# END handle quotations
|
| 527 |
+
optval = cursect.getlast(optname)
|
| 528 |
+
cursect.setlast(optname, optval + string_decode(line))
|
| 529 |
+
# END parse section or option
|
| 530 |
+
# END while reading
|
| 531 |
+
|
| 532 |
+
# If any parsing errors occurred, raise an exception.
|
| 533 |
+
if e:
|
| 534 |
+
raise e
|
| 535 |
+
|
| 536 |
+
def _has_includes(self) -> Union[bool, int]:
|
| 537 |
+
return self._merge_includes and len(self._included_paths())
|
| 538 |
+
|
| 539 |
+
def _included_paths(self) -> List[Tuple[str, str]]:
|
| 540 |
+
"""List all paths that must be included to configuration.
|
| 541 |
+
|
| 542 |
+
:return:
|
| 543 |
+
The list of paths, where each path is a tuple of (option, value).
|
| 544 |
+
"""
|
| 545 |
+
paths = []
|
| 546 |
+
|
| 547 |
+
for section in self.sections():
|
| 548 |
+
if section == "include":
|
| 549 |
+
paths += self.items(section)
|
| 550 |
+
|
| 551 |
+
match = CONDITIONAL_INCLUDE_REGEXP.search(section)
|
| 552 |
+
if match is None or self._repo is None:
|
| 553 |
+
continue
|
| 554 |
+
|
| 555 |
+
keyword = match.group(1)
|
| 556 |
+
value = match.group(2).strip()
|
| 557 |
+
|
| 558 |
+
if keyword in ["gitdir", "gitdir/i"]:
|
| 559 |
+
value = osp.expanduser(value)
|
| 560 |
+
|
| 561 |
+
if not any(value.startswith(s) for s in ["./", "/"]):
|
| 562 |
+
value = "**/" + value
|
| 563 |
+
if value.endswith("/"):
|
| 564 |
+
value += "**"
|
| 565 |
+
|
| 566 |
+
# Ensure that glob is always case insensitive if required.
|
| 567 |
+
if keyword.endswith("/i"):
|
| 568 |
+
value = re.sub(
|
| 569 |
+
r"[a-zA-Z]",
|
| 570 |
+
lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()),
|
| 571 |
+
value,
|
| 572 |
+
)
|
| 573 |
+
if self._repo.git_dir:
|
| 574 |
+
if fnmatch.fnmatchcase(str(self._repo.git_dir), value):
|
| 575 |
+
paths += self.items(section)
|
| 576 |
+
|
| 577 |
+
elif keyword == "onbranch":
|
| 578 |
+
try:
|
| 579 |
+
branch_name = self._repo.active_branch.name
|
| 580 |
+
except TypeError:
|
| 581 |
+
# Ignore section if active branch cannot be retrieved.
|
| 582 |
+
continue
|
| 583 |
+
|
| 584 |
+
if fnmatch.fnmatchcase(branch_name, value):
|
| 585 |
+
paths += self.items(section)
|
| 586 |
+
|
| 587 |
+
return paths
|
| 588 |
+
|
| 589 |
+
def read(self) -> None: # type: ignore[override]
|
| 590 |
+
"""Read the data stored in the files we have been initialized with.
|
| 591 |
+
|
| 592 |
+
This will ignore files that cannot be read, possibly leaving an empty
|
| 593 |
+
configuration.
|
| 594 |
+
|
| 595 |
+
:raise IOError:
|
| 596 |
+
If a file cannot be handled.
|
| 597 |
+
"""
|
| 598 |
+
if self._is_initialized:
|
| 599 |
+
return
|
| 600 |
+
self._is_initialized = True
|
| 601 |
+
|
| 602 |
+
files_to_read: List[Union[PathLike, IO]] = [""]
|
| 603 |
+
if isinstance(self._file_or_files, (str, os.PathLike)):
|
| 604 |
+
# For str or Path, as str is a type of Sequence.
|
| 605 |
+
files_to_read = [self._file_or_files]
|
| 606 |
+
elif not isinstance(self._file_or_files, (tuple, list, Sequence)):
|
| 607 |
+
# Could merge with above isinstance once runtime type known.
|
| 608 |
+
files_to_read = [self._file_or_files]
|
| 609 |
+
else: # For lists or tuples.
|
| 610 |
+
files_to_read = list(self._file_or_files)
|
| 611 |
+
# END ensure we have a copy of the paths to handle
|
| 612 |
+
|
| 613 |
+
seen = set(files_to_read)
|
| 614 |
+
num_read_include_files = 0
|
| 615 |
+
while files_to_read:
|
| 616 |
+
file_path = files_to_read.pop(0)
|
| 617 |
+
file_ok = False
|
| 618 |
+
|
| 619 |
+
if hasattr(file_path, "seek"):
|
| 620 |
+
# Must be a file-object.
|
| 621 |
+
# TODO: Replace cast with assert to narrow type, once sure.
|
| 622 |
+
file_path = cast(IO[bytes], file_path)
|
| 623 |
+
self._read(file_path, file_path.name)
|
| 624 |
+
else:
|
| 625 |
+
# Assume a path if it is not a file-object.
|
| 626 |
+
file_path = cast(PathLike, file_path)
|
| 627 |
+
try:
|
| 628 |
+
with open(file_path, "rb") as fp:
|
| 629 |
+
file_ok = True
|
| 630 |
+
self._read(fp, fp.name)
|
| 631 |
+
except IOError:
|
| 632 |
+
continue
|
| 633 |
+
|
| 634 |
+
# Read includes and append those that we didn't handle yet. We expect all
|
| 635 |
+
# paths to be normalized and absolute (and will ensure that is the case).
|
| 636 |
+
if self._has_includes():
|
| 637 |
+
for _, include_path in self._included_paths():
|
| 638 |
+
if include_path.startswith("~"):
|
| 639 |
+
include_path = osp.expanduser(include_path)
|
| 640 |
+
if not osp.isabs(include_path):
|
| 641 |
+
if not file_ok:
|
| 642 |
+
continue
|
| 643 |
+
# END ignore relative paths if we don't know the configuration file path
|
| 644 |
+
file_path = cast(PathLike, file_path)
|
| 645 |
+
assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
|
| 646 |
+
include_path = osp.join(osp.dirname(file_path), include_path)
|
| 647 |
+
# END make include path absolute
|
| 648 |
+
include_path = osp.normpath(include_path)
|
| 649 |
+
if include_path in seen or not os.access(include_path, os.R_OK):
|
| 650 |
+
continue
|
| 651 |
+
seen.add(include_path)
|
| 652 |
+
# Insert included file to the top to be considered first.
|
| 653 |
+
files_to_read.insert(0, include_path)
|
| 654 |
+
num_read_include_files += 1
|
| 655 |
+
# END each include path in configuration file
|
| 656 |
+
# END handle includes
|
| 657 |
+
# END for each file object to read
|
| 658 |
+
|
| 659 |
+
# If there was no file included, we can safely write back (potentially) the
|
| 660 |
+
# configuration file without altering its meaning.
|
| 661 |
+
if num_read_include_files == 0:
|
| 662 |
+
self._merge_includes = False
|
| 663 |
+
|
| 664 |
+
def _write(self, fp: IO) -> None:
|
| 665 |
+
"""Write an .ini-format representation of the configuration state in
|
| 666 |
+
git compatible format."""
|
| 667 |
+
|
| 668 |
+
def write_section(name: str, section_dict: _OMD) -> None:
|
| 669 |
+
fp.write(("[%s]\n" % name).encode(defenc))
|
| 670 |
+
|
| 671 |
+
values: Sequence[str] # Runtime only gets str in tests, but should be whatever _OMD stores.
|
| 672 |
+
v: str
|
| 673 |
+
for key, values in section_dict.items_all():
|
| 674 |
+
if key == "__name__":
|
| 675 |
+
continue
|
| 676 |
+
|
| 677 |
+
for v in values:
|
| 678 |
+
fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
|
| 679 |
+
# END if key is not __name__
|
| 680 |
+
|
| 681 |
+
# END section writing
|
| 682 |
+
|
| 683 |
+
if self._defaults:
|
| 684 |
+
write_section(cp.DEFAULTSECT, self._defaults)
|
| 685 |
+
value: _OMD
|
| 686 |
+
|
| 687 |
+
for name, value in self._sections.items():
|
| 688 |
+
write_section(name, value)
|
| 689 |
+
|
| 690 |
+
def items(self, section_name: str) -> List[Tuple[str, str]]: # type: ignore[override]
|
| 691 |
+
""":return: list((option, value), ...) pairs of all items in the given section"""
|
| 692 |
+
return [(k, v) for k, v in super().items(section_name) if k != "__name__"]
|
| 693 |
+
|
| 694 |
+
def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
|
| 695 |
+
""":return: list((option, [values...]), ...) pairs of all items in the given section"""
|
| 696 |
+
rv = _OMD(self._defaults)
|
| 697 |
+
|
| 698 |
+
for k, vs in self._sections[section_name].items_all():
|
| 699 |
+
if k == "__name__":
|
| 700 |
+
continue
|
| 701 |
+
|
| 702 |
+
if k in rv and rv.getall(k) == vs:
|
| 703 |
+
continue
|
| 704 |
+
|
| 705 |
+
for v in vs:
|
| 706 |
+
rv.add(k, v)
|
| 707 |
+
|
| 708 |
+
return rv.items_all()
|
| 709 |
+
|
| 710 |
+
@needs_values
|
| 711 |
+
def write(self) -> None:
|
| 712 |
+
"""Write changes to our file, if there are changes at all.
|
| 713 |
+
|
| 714 |
+
:raise IOError:
|
| 715 |
+
If this is a read-only writer instance or if we could not obtain a file
|
| 716 |
+
lock.
|
| 717 |
+
"""
|
| 718 |
+
self._assure_writable("write")
|
| 719 |
+
if not self._dirty:
|
| 720 |
+
return
|
| 721 |
+
|
| 722 |
+
if isinstance(self._file_or_files, (list, tuple)):
|
| 723 |
+
raise AssertionError(
|
| 724 |
+
"Cannot write back if there is not exactly a single file to write to, have %i files"
|
| 725 |
+
% len(self._file_or_files)
|
| 726 |
+
)
|
| 727 |
+
# END assert multiple files
|
| 728 |
+
|
| 729 |
+
if self._has_includes():
|
| 730 |
+
_logger.debug(
|
| 731 |
+
"Skipping write-back of configuration file as include files were merged in."
|
| 732 |
+
+ "Set merge_includes=False to prevent this."
|
| 733 |
+
)
|
| 734 |
+
return
|
| 735 |
+
# END stop if we have include files
|
| 736 |
+
|
| 737 |
+
fp = self._file_or_files
|
| 738 |
+
|
| 739 |
+
# We have a physical file on disk, so get a lock.
|
| 740 |
+
is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # TODO: Use PathLike (having dropped 3.5).
|
| 741 |
+
if is_file_lock and self._lock is not None: # Else raise error?
|
| 742 |
+
self._lock._obtain_lock()
|
| 743 |
+
|
| 744 |
+
if not hasattr(fp, "seek"):
|
| 745 |
+
fp = cast(PathLike, fp)
|
| 746 |
+
with open(fp, "wb") as fp_open:
|
| 747 |
+
self._write(fp_open)
|
| 748 |
+
else:
|
| 749 |
+
fp = cast("BytesIO", fp)
|
| 750 |
+
fp.seek(0)
|
| 751 |
+
# Make sure we do not overwrite into an existing file.
|
| 752 |
+
if hasattr(fp, "truncate"):
|
| 753 |
+
fp.truncate()
|
| 754 |
+
self._write(fp)
|
| 755 |
+
|
| 756 |
+
def _assure_writable(self, method_name: str) -> None:
|
| 757 |
+
if self.read_only:
|
| 758 |
+
raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
|
| 759 |
+
|
| 760 |
+
def add_section(self, section: str) -> None:
|
| 761 |
+
"""Assures added options will stay in order."""
|
| 762 |
+
return super().add_section(section)
|
| 763 |
+
|
| 764 |
+
@property
|
| 765 |
+
def read_only(self) -> bool:
|
| 766 |
+
""":return: ``True`` if this instance may change the configuration file"""
|
| 767 |
+
return self._read_only
|
| 768 |
+
|
| 769 |
+
# FIXME: Figure out if default or return type can really include bool.
|
| 770 |
+
def get_value(
|
| 771 |
+
self,
|
| 772 |
+
section: str,
|
| 773 |
+
option: str,
|
| 774 |
+
default: Union[int, float, str, bool, None] = None,
|
| 775 |
+
) -> Union[int, float, str, bool]:
|
| 776 |
+
"""Get an option's value.
|
| 777 |
+
|
| 778 |
+
If multiple values are specified for this option in the section, the last one
|
| 779 |
+
specified is returned.
|
| 780 |
+
|
| 781 |
+
:param default:
|
| 782 |
+
If not ``None``, the given default value will be returned in case the option
|
| 783 |
+
did not exist.
|
| 784 |
+
|
| 785 |
+
:return:
|
| 786 |
+
A properly typed value, either int, float or string
|
| 787 |
+
|
| 788 |
+
:raise TypeError:
|
| 789 |
+
In case the value could not be understood.
|
| 790 |
+
Otherwise the exceptions known to the ConfigParser will be raised.
|
| 791 |
+
"""
|
| 792 |
+
try:
|
| 793 |
+
valuestr = self.get(section, option)
|
| 794 |
+
except Exception:
|
| 795 |
+
if default is not None:
|
| 796 |
+
return default
|
| 797 |
+
raise
|
| 798 |
+
|
| 799 |
+
return self._string_to_value(valuestr)
|
| 800 |
+
|
| 801 |
+
def get_values(
|
| 802 |
+
self,
|
| 803 |
+
section: str,
|
| 804 |
+
option: str,
|
| 805 |
+
default: Union[int, float, str, bool, None] = None,
|
| 806 |
+
) -> List[Union[int, float, str, bool]]:
|
| 807 |
+
"""Get an option's values.
|
| 808 |
+
|
| 809 |
+
If multiple values are specified for this option in the section, all are
|
| 810 |
+
returned.
|
| 811 |
+
|
| 812 |
+
:param default:
|
| 813 |
+
If not ``None``, a list containing the given default value will be returned
|
| 814 |
+
in case the option did not exist.
|
| 815 |
+
|
| 816 |
+
:return:
|
| 817 |
+
A list of properly typed values, either int, float or string
|
| 818 |
+
|
| 819 |
+
:raise TypeError:
|
| 820 |
+
In case the value could not be understood.
|
| 821 |
+
Otherwise the exceptions known to the ConfigParser will be raised.
|
| 822 |
+
"""
|
| 823 |
+
try:
|
| 824 |
+
self.sections()
|
| 825 |
+
lst = self._sections[section].getall(option)
|
| 826 |
+
except Exception:
|
| 827 |
+
if default is not None:
|
| 828 |
+
return [default]
|
| 829 |
+
raise
|
| 830 |
+
|
| 831 |
+
return [self._string_to_value(valuestr) for valuestr in lst]
|
| 832 |
+
|
| 833 |
+
def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]:
|
| 834 |
+
types = (int, float)
|
| 835 |
+
for numtype in types:
|
| 836 |
+
try:
|
| 837 |
+
val = numtype(valuestr)
|
| 838 |
+
# truncated value ?
|
| 839 |
+
if val != float(valuestr):
|
| 840 |
+
continue
|
| 841 |
+
return val
|
| 842 |
+
except (ValueError, TypeError):
|
| 843 |
+
continue
|
| 844 |
+
# END for each numeric type
|
| 845 |
+
|
| 846 |
+
# Try boolean values as git uses them.
|
| 847 |
+
vl = valuestr.lower()
|
| 848 |
+
if vl == "false":
|
| 849 |
+
return False
|
| 850 |
+
if vl == "true":
|
| 851 |
+
return True
|
| 852 |
+
|
| 853 |
+
if not isinstance(valuestr, str):
|
| 854 |
+
raise TypeError(
|
| 855 |
+
"Invalid value type: only int, long, float and str are allowed",
|
| 856 |
+
valuestr,
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
return valuestr
|
| 860 |
+
|
| 861 |
+
def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str:
|
| 862 |
+
if isinstance(value, (int, float, bool)):
|
| 863 |
+
return str(value)
|
| 864 |
+
return force_text(value)
|
| 865 |
+
|
| 866 |
+
@needs_values
|
| 867 |
+
@set_dirty_and_flush_changes
|
| 868 |
+
def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
|
| 869 |
+
"""Set the given option in section to the given value.
|
| 870 |
+
|
| 871 |
+
This will create the section if required, and will not throw as opposed to the
|
| 872 |
+
default ConfigParser ``set`` method.
|
| 873 |
+
|
| 874 |
+
:param section:
|
| 875 |
+
Name of the section in which the option resides or should reside.
|
| 876 |
+
|
| 877 |
+
:param option:
|
| 878 |
+
Name of the options whose value to set.
|
| 879 |
+
|
| 880 |
+
:param value:
|
| 881 |
+
Value to set the option to. It must be a string or convertible to a string.
|
| 882 |
+
|
| 883 |
+
:return:
|
| 884 |
+
This instance
|
| 885 |
+
"""
|
| 886 |
+
if not self.has_section(section):
|
| 887 |
+
self.add_section(section)
|
| 888 |
+
self.set(section, option, self._value_to_string(value))
|
| 889 |
+
return self
|
| 890 |
+
|
| 891 |
+
@needs_values
|
| 892 |
+
@set_dirty_and_flush_changes
|
| 893 |
+
def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
|
| 894 |
+
"""Add a value for the given option in section.
|
| 895 |
+
|
| 896 |
+
This will create the section if required, and will not throw as opposed to the
|
| 897 |
+
default ConfigParser ``set`` method. The value becomes the new value of the
|
| 898 |
+
option as returned by :meth:`get_value`, and appends to the list of values
|
| 899 |
+
returned by :meth:`get_values`.
|
| 900 |
+
|
| 901 |
+
:param section:
|
| 902 |
+
Name of the section in which the option resides or should reside.
|
| 903 |
+
|
| 904 |
+
:param option:
|
| 905 |
+
Name of the option.
|
| 906 |
+
|
| 907 |
+
:param value:
|
| 908 |
+
Value to add to option. It must be a string or convertible to a string.
|
| 909 |
+
|
| 910 |
+
:return:
|
| 911 |
+
This instance
|
| 912 |
+
"""
|
| 913 |
+
if not self.has_section(section):
|
| 914 |
+
self.add_section(section)
|
| 915 |
+
self._sections[section].add(option, self._value_to_string(value))
|
| 916 |
+
return self
|
| 917 |
+
|
| 918 |
+
def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
|
| 919 |
+
"""Rename the given section to `new_name`.
|
| 920 |
+
|
| 921 |
+
:raise ValueError:
|
| 922 |
+
If:
|
| 923 |
+
|
| 924 |
+
* `section` doesn't exist.
|
| 925 |
+
* A section with `new_name` does already exist.
|
| 926 |
+
|
| 927 |
+
:return:
|
| 928 |
+
This instance
|
| 929 |
+
"""
|
| 930 |
+
if not self.has_section(section):
|
| 931 |
+
raise ValueError("Source section '%s' doesn't exist" % section)
|
| 932 |
+
if self.has_section(new_name):
|
| 933 |
+
raise ValueError("Destination section '%s' already exists" % new_name)
|
| 934 |
+
|
| 935 |
+
super().add_section(new_name)
|
| 936 |
+
new_section = self._sections[new_name]
|
| 937 |
+
for k, vs in self.items_all(section):
|
| 938 |
+
new_section.setall(k, vs)
|
| 939 |
+
# END for each value to copy
|
| 940 |
+
|
| 941 |
+
# This call writes back the changes, which is why we don't have the respective
|
| 942 |
+
# decorator.
|
| 943 |
+
self.remove_section(section)
|
| 944 |
+
return self
|
venv/lib/python3.10/site-packages/git/db.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
"""Module with our own gitdb implementation - it uses the git command."""
|
| 5 |
+
|
| 6 |
+
__all__ = ["GitCmdObjectDB", "GitDB"]
|
| 7 |
+
|
| 8 |
+
from gitdb.base import OInfo, OStream
|
| 9 |
+
from gitdb.db import GitDB, LooseObjectDB
|
| 10 |
+
from gitdb.exc import BadObject
|
| 11 |
+
|
| 12 |
+
from git.util import bin_to_hex, hex_to_bin
|
| 13 |
+
from git.exc import GitCommandError
|
| 14 |
+
|
| 15 |
+
# typing-------------------------------------------------
|
| 16 |
+
|
| 17 |
+
from typing import TYPE_CHECKING
|
| 18 |
+
|
| 19 |
+
from git.types import PathLike
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from git.cmd import Git
|
| 23 |
+
|
| 24 |
+
# --------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class GitCmdObjectDB(LooseObjectDB):
|
| 28 |
+
"""A database representing the default git object store, which includes loose
|
| 29 |
+
objects, pack files and an alternates file.
|
| 30 |
+
|
| 31 |
+
It will create objects only in the loose object database.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, root_path: PathLike, git: "Git") -> None:
|
| 35 |
+
"""Initialize this instance with the root and a git command."""
|
| 36 |
+
super().__init__(root_path)
|
| 37 |
+
self._git = git
|
| 38 |
+
|
| 39 |
+
def info(self, binsha: bytes) -> OInfo:
|
| 40 |
+
"""Get a git object header (using git itself)."""
|
| 41 |
+
hexsha, typename, size = self._git.get_object_header(bin_to_hex(binsha))
|
| 42 |
+
return OInfo(hex_to_bin(hexsha), typename, size)
|
| 43 |
+
|
| 44 |
+
def stream(self, binsha: bytes) -> OStream:
|
| 45 |
+
"""Get git object data as a stream supporting ``read()`` (using git itself)."""
|
| 46 |
+
hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha))
|
| 47 |
+
return OStream(hex_to_bin(hexsha), typename, size, stream)
|
| 48 |
+
|
| 49 |
+
# { Interface
|
| 50 |
+
|
| 51 |
+
def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
|
| 52 |
+
"""
|
| 53 |
+
:return:
|
| 54 |
+
Full binary 20 byte sha from the given partial hexsha
|
| 55 |
+
|
| 56 |
+
:raise gitdb.exc.AmbiguousObjectName:
|
| 57 |
+
|
| 58 |
+
:raise gitdb.exc.BadObject:
|
| 59 |
+
|
| 60 |
+
:note:
|
| 61 |
+
Currently we only raise :exc:`~gitdb.exc.BadObject` as git does not
|
| 62 |
+
communicate ambiguous objects separately.
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)
|
| 66 |
+
return hex_to_bin(hexsha)
|
| 67 |
+
except (GitCommandError, ValueError) as e:
|
| 68 |
+
raise BadObject(partial_hexsha) from e
|
| 69 |
+
# END handle exceptions
|
| 70 |
+
|
| 71 |
+
# } END interface
|
venv/lib/python3.10/site-packages/git/diff.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
__all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"]
|
| 7 |
+
|
| 8 |
+
import enum
|
| 9 |
+
import re
|
| 10 |
+
import warnings
|
| 11 |
+
|
| 12 |
+
from git.cmd import handle_process_output
|
| 13 |
+
from git.compat import defenc
|
| 14 |
+
from git.objects.blob import Blob
|
| 15 |
+
from git.objects.util import mode_str_to_int
|
| 16 |
+
from git.util import finalize_process, hex_to_bin
|
| 17 |
+
|
| 18 |
+
# typing ------------------------------------------------------------------
|
| 19 |
+
|
| 20 |
+
from typing import (
|
| 21 |
+
Any,
|
| 22 |
+
Iterator,
|
| 23 |
+
List,
|
| 24 |
+
Match,
|
| 25 |
+
Optional,
|
| 26 |
+
Tuple,
|
| 27 |
+
TYPE_CHECKING,
|
| 28 |
+
TypeVar,
|
| 29 |
+
Union,
|
| 30 |
+
cast,
|
| 31 |
+
)
|
| 32 |
+
from git.types import Literal, PathLike
|
| 33 |
+
|
| 34 |
+
if TYPE_CHECKING:
|
| 35 |
+
from subprocess import Popen
|
| 36 |
+
|
| 37 |
+
from git.cmd import Git
|
| 38 |
+
from git.objects.base import IndexObject
|
| 39 |
+
from git.objects.commit import Commit
|
| 40 |
+
from git.objects.tree import Tree
|
| 41 |
+
from git.repo.base import Repo
|
| 42 |
+
|
| 43 |
+
Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
|
| 44 |
+
|
| 45 |
+
# ------------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@enum.unique
|
| 49 |
+
class DiffConstants(enum.Enum):
|
| 50 |
+
"""Special objects for :meth:`Diffable.diff`.
|
| 51 |
+
|
| 52 |
+
See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
|
| 53 |
+
values including these.
|
| 54 |
+
|
| 55 |
+
:note:
|
| 56 |
+
These constants are also available as attributes of the :mod:`git.diff` module,
|
| 57 |
+
the :class:`Diffable` class and its subclasses and instances, and the top-level
|
| 58 |
+
:mod:`git` module.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
NULL_TREE = enum.auto()
|
| 62 |
+
"""Stand-in indicating you want to compare against the empty tree in diffs.
|
| 63 |
+
|
| 64 |
+
Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
|
| 65 |
+
:const:`Diffable.NULL_TREE`.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
INDEX = enum.auto()
|
| 69 |
+
"""Stand-in indicating you want to diff against the index.
|
| 70 |
+
|
| 71 |
+
Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
|
| 72 |
+
:const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
|
| 73 |
+
kept for backward compatibility and made an alias of this, so it may still be used.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
|
| 78 |
+
"""Stand-in indicating you want to compare against the empty tree in diffs.
|
| 79 |
+
|
| 80 |
+
See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
|
| 81 |
+
|
| 82 |
+
This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
|
| 83 |
+
:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
|
| 87 |
+
"""Stand-in indicating you want to diff against the index.
|
| 88 |
+
|
| 89 |
+
See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
|
| 90 |
+
|
| 91 |
+
This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
|
| 92 |
+
:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
_octal_byte_re = re.compile(rb"\\([0-9]{3})")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _octal_repl(matchobj: Match) -> bytes:
|
| 99 |
+
value = matchobj.group(1)
|
| 100 |
+
value = int(value, 8)
|
| 101 |
+
value = bytes(bytearray((value,)))
|
| 102 |
+
return value
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
|
| 106 |
+
if path == b"/dev/null":
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
if path.startswith(b'"') and path.endswith(b'"'):
|
| 110 |
+
path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\")
|
| 111 |
+
|
| 112 |
+
path = _octal_byte_re.sub(_octal_repl, path)
|
| 113 |
+
|
| 114 |
+
if has_ab_prefix:
|
| 115 |
+
assert path.startswith(b"a/") or path.startswith(b"b/")
|
| 116 |
+
path = path[2:]
|
| 117 |
+
|
| 118 |
+
return path
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class Diffable:
|
| 122 |
+
"""Common interface for all objects that can be diffed against another object of
|
| 123 |
+
compatible type.
|
| 124 |
+
|
| 125 |
+
:note:
|
| 126 |
+
Subclasses require a :attr:`repo` member, as it is the case for
|
| 127 |
+
:class:`~git.objects.base.Object` instances. For practical reasons we do not
|
| 128 |
+
derive from :class:`~git.objects.base.Object`.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
__slots__ = ()
|
| 132 |
+
|
| 133 |
+
repo: "Repo"
|
| 134 |
+
"""Repository to operate on. Must be provided by subclass or sibling class."""
|
| 135 |
+
|
| 136 |
+
NULL_TREE = NULL_TREE
|
| 137 |
+
"""Stand-in indicating you want to compare against the empty tree in diffs.
|
| 138 |
+
|
| 139 |
+
See the :meth:`diff` method, which accepts this as a value of its ``other``
|
| 140 |
+
parameter.
|
| 141 |
+
|
| 142 |
+
This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
|
| 143 |
+
:const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
INDEX = INDEX
|
| 147 |
+
"""Stand-in indicating you want to diff against the index.
|
| 148 |
+
|
| 149 |
+
See the :meth:`diff` method, which accepts this as a value of its ``other``
|
| 150 |
+
parameter.
|
| 151 |
+
|
| 152 |
+
This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
|
| 153 |
+
:const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
|
| 154 |
+
which is kept for backward compatibility (it is now defined an alias of this).
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
Index = INDEX
|
| 158 |
+
"""Stand-in indicating you want to diff against the index
|
| 159 |
+
(same as :const:`~Diffable.INDEX`).
|
| 160 |
+
|
| 161 |
+
This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
|
| 162 |
+
:const:`~Diffable.INDEX` and :meth:`diff` for details.
|
| 163 |
+
|
| 164 |
+
:note:
|
| 165 |
+
Although always meant for use as an opaque constant, this was formerly defined
|
| 166 |
+
as a class. Its usage is unchanged, but static type annotations that attempt
|
| 167 |
+
to permit only this object must be changed to avoid new mypy errors. This was
|
| 168 |
+
previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
|
| 169 |
+
It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
def _process_diff_args(
|
| 173 |
+
self,
|
| 174 |
+
args: List[Union[PathLike, "Diffable"]],
|
| 175 |
+
) -> List[Union[PathLike, "Diffable"]]:
|
| 176 |
+
"""
|
| 177 |
+
:return:
|
| 178 |
+
Possibly altered version of the given args list.
|
| 179 |
+
This method is called right before git command execution.
|
| 180 |
+
Subclasses can use it to alter the behaviour of the superclass.
|
| 181 |
+
"""
|
| 182 |
+
return args
|
| 183 |
+
|
| 184 |
+
def diff(
|
| 185 |
+
self,
|
| 186 |
+
other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
|
| 187 |
+
paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
|
| 188 |
+
create_patch: bool = False,
|
| 189 |
+
**kwargs: Any,
|
| 190 |
+
) -> "DiffIndex[Diff]":
|
| 191 |
+
"""Create diffs between two items being trees, trees and index or an index and
|
| 192 |
+
the working tree. Detects renames automatically.
|
| 193 |
+
|
| 194 |
+
:param other:
|
| 195 |
+
This the item to compare us with.
|
| 196 |
+
|
| 197 |
+
* If ``None``, we will be compared to the working tree.
|
| 198 |
+
|
| 199 |
+
* If a :class:`~git.types.Tree_ish` or string, it will be compared against
|
| 200 |
+
the respective tree.
|
| 201 |
+
|
| 202 |
+
* If :const:`INDEX`, it will be compared against the index.
|
| 203 |
+
|
| 204 |
+
* If :const:`NULL_TREE`, it will compare against the empty tree.
|
| 205 |
+
|
| 206 |
+
This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
|
| 207 |
+
method will not by default fail on bare repositories.
|
| 208 |
+
|
| 209 |
+
:param paths:
|
| 210 |
+
This a list of paths or a single path to limit the diff to. It will only
|
| 211 |
+
include at least one of the given path or paths.
|
| 212 |
+
|
| 213 |
+
:param create_patch:
|
| 214 |
+
If ``True``, the returned :class:`Diff` contains a detailed patch that if
|
| 215 |
+
applied makes the self to other. Patches are somewhat costly as blobs have
|
| 216 |
+
to be read and diffed.
|
| 217 |
+
|
| 218 |
+
:param kwargs:
|
| 219 |
+
Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
|
| 220 |
+
swap both sides of the diff.
|
| 221 |
+
|
| 222 |
+
:return:
|
| 223 |
+
A :class:`DiffIndex` representing the computed diff.
|
| 224 |
+
|
| 225 |
+
:note:
|
| 226 |
+
On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
|
| 227 |
+
an instance of :class:`~git.objects.tree.Tree` or
|
| 228 |
+
:class:`~git.objects.commit.Commit`, or a git command error will occur.
|
| 229 |
+
"""
|
| 230 |
+
args: List[Union[PathLike, Diffable]] = []
|
| 231 |
+
args.append("--abbrev=40") # We need full shas.
|
| 232 |
+
args.append("--full-index") # Get full index paths, not only filenames.
|
| 233 |
+
|
| 234 |
+
# Remove default '-M' arg (check for renames) if user is overriding it.
|
| 235 |
+
if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
|
| 236 |
+
args.append("-M")
|
| 237 |
+
|
| 238 |
+
if create_patch:
|
| 239 |
+
args.append("-p")
|
| 240 |
+
args.append("--no-ext-diff")
|
| 241 |
+
else:
|
| 242 |
+
args.append("--raw")
|
| 243 |
+
args.append("-z")
|
| 244 |
+
|
| 245 |
+
# Ensure we never see colored output.
|
| 246 |
+
# Fixes: https://github.com/gitpython-developers/GitPython/issues/172
|
| 247 |
+
args.append("--no-color")
|
| 248 |
+
|
| 249 |
+
if paths is not None and not isinstance(paths, (tuple, list)):
|
| 250 |
+
paths = [paths]
|
| 251 |
+
|
| 252 |
+
diff_cmd = self.repo.git.diff
|
| 253 |
+
if other is INDEX:
|
| 254 |
+
args.insert(0, "--cached")
|
| 255 |
+
elif other is NULL_TREE:
|
| 256 |
+
args.insert(0, "-r") # Recursive diff-tree.
|
| 257 |
+
args.insert(0, "--root")
|
| 258 |
+
diff_cmd = self.repo.git.diff_tree
|
| 259 |
+
elif other is not None:
|
| 260 |
+
args.insert(0, "-r") # Recursive diff-tree.
|
| 261 |
+
args.insert(0, other)
|
| 262 |
+
diff_cmd = self.repo.git.diff_tree
|
| 263 |
+
|
| 264 |
+
args.insert(0, self)
|
| 265 |
+
|
| 266 |
+
# paths is a list or tuple here, or None.
|
| 267 |
+
if paths:
|
| 268 |
+
args.append("--")
|
| 269 |
+
args.extend(paths)
|
| 270 |
+
# END paths handling
|
| 271 |
+
|
| 272 |
+
kwargs["as_process"] = True
|
| 273 |
+
proc = diff_cmd(*self._process_diff_args(args), **kwargs)
|
| 274 |
+
|
| 275 |
+
diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format
|
| 276 |
+
index = diff_method(self.repo, proc)
|
| 277 |
+
|
| 278 |
+
proc.wait()
|
| 279 |
+
return index
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
T_Diff = TypeVar("T_Diff", bound="Diff")
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
class DiffIndex(List[T_Diff]):
|
| 286 |
+
R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
|
| 287 |
+
properties.
|
| 288 |
+
|
| 289 |
+
The class improves the diff handling convenience.
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
+
change_type = ("A", "C", "D", "R", "M", "T")
|
| 293 |
+
"""Change type invariant identifying possible ways a blob can have changed:
|
| 294 |
+
|
| 295 |
+
* ``A`` = Added
|
| 296 |
+
* ``D`` = Deleted
|
| 297 |
+
* ``R`` = Renamed
|
| 298 |
+
* ``M`` = Modified
|
| 299 |
+
* ``T`` = Changed in the type
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
|
| 303 |
+
"""
|
| 304 |
+
:return:
|
| 305 |
+
Iterator yielding :class:`Diff` instances that match the given `change_type`
|
| 306 |
+
|
| 307 |
+
:param change_type:
|
| 308 |
+
Member of :attr:`DiffIndex.change_type`, namely:
|
| 309 |
+
|
| 310 |
+
* 'A' for added paths
|
| 311 |
+
* 'D' for deleted paths
|
| 312 |
+
* 'R' for renamed paths
|
| 313 |
+
* 'M' for paths with modified data
|
| 314 |
+
* 'T' for changed in the type paths
|
| 315 |
+
"""
|
| 316 |
+
if change_type not in self.change_type:
|
| 317 |
+
raise ValueError("Invalid change type: %s" % change_type)
|
| 318 |
+
|
| 319 |
+
for diffidx in self:
|
| 320 |
+
if diffidx.change_type == change_type:
|
| 321 |
+
yield diffidx
|
| 322 |
+
elif change_type == "A" and diffidx.new_file:
|
| 323 |
+
yield diffidx
|
| 324 |
+
elif change_type == "D" and diffidx.deleted_file:
|
| 325 |
+
yield diffidx
|
| 326 |
+
elif change_type == "C" and diffidx.copied_file:
|
| 327 |
+
yield diffidx
|
| 328 |
+
elif change_type == "R" and diffidx.renamed_file:
|
| 329 |
+
yield diffidx
|
| 330 |
+
elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
|
| 331 |
+
yield diffidx
|
| 332 |
+
# END for each diff
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class Diff:
|
| 336 |
+
"""A Diff contains diff information between two Trees.
|
| 337 |
+
|
| 338 |
+
It contains two sides a and b of the diff. Members are prefixed with "a" and "b"
|
| 339 |
+
respectively to indicate that.
|
| 340 |
+
|
| 341 |
+
Diffs keep information about the changed blob objects, the file mode, renames,
|
| 342 |
+
deletions and new files.
|
| 343 |
+
|
| 344 |
+
There are a few cases where ``None`` has to be expected as member variable value:
|
| 345 |
+
|
| 346 |
+
New File::
|
| 347 |
+
|
| 348 |
+
a_mode is None
|
| 349 |
+
a_blob is None
|
| 350 |
+
a_path is None
|
| 351 |
+
|
| 352 |
+
Deleted File::
|
| 353 |
+
|
| 354 |
+
b_mode is None
|
| 355 |
+
b_blob is None
|
| 356 |
+
b_path is None
|
| 357 |
+
|
| 358 |
+
Working Tree Blobs:
|
| 359 |
+
|
| 360 |
+
When comparing to working trees, the working tree blob will have a null hexsha
|
| 361 |
+
as a corresponding object does not yet exist. The mode will be null as well. The
|
| 362 |
+
path will be available, though.
|
| 363 |
+
|
| 364 |
+
If it is listed in a diff, the working tree version of the file must differ from
|
| 365 |
+
the version in the index or tree, and hence has been modified.
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
# Precompiled regex.
|
| 369 |
+
re_header = re.compile(
|
| 370 |
+
rb"""
|
| 371 |
+
^diff[ ]--git
|
| 372 |
+
[ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n
|
| 373 |
+
(?:^old[ ]mode[ ](?P<old_mode>\d+)\n
|
| 374 |
+
^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
|
| 375 |
+
(?:^similarity[ ]index[ ]\d+%\n
|
| 376 |
+
^rename[ ]from[ ](?P<rename_from>.*)\n
|
| 377 |
+
^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))?
|
| 378 |
+
(?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
|
| 379 |
+
(?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
|
| 380 |
+
(?:^similarity[ ]index[ ]\d+%\n
|
| 381 |
+
^copy[ ]from[ ].*\n
|
| 382 |
+
^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))?
|
| 383 |
+
(?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
|
| 384 |
+
\.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
|
| 385 |
+
(?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
|
| 386 |
+
(?:^\+\+\+[ ](?P<b_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
|
| 387 |
+
""",
|
| 388 |
+
re.VERBOSE | re.MULTILINE,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# These can be used for comparisons.
|
| 392 |
+
NULL_HEX_SHA = "0" * 40
|
| 393 |
+
NULL_BIN_SHA = b"\0" * 20
|
| 394 |
+
|
| 395 |
+
__slots__ = (
|
| 396 |
+
"a_blob",
|
| 397 |
+
"b_blob",
|
| 398 |
+
"a_mode",
|
| 399 |
+
"b_mode",
|
| 400 |
+
"a_rawpath",
|
| 401 |
+
"b_rawpath",
|
| 402 |
+
"new_file",
|
| 403 |
+
"deleted_file",
|
| 404 |
+
"copied_file",
|
| 405 |
+
"raw_rename_from",
|
| 406 |
+
"raw_rename_to",
|
| 407 |
+
"diff",
|
| 408 |
+
"change_type",
|
| 409 |
+
"score",
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
def __init__(
|
| 413 |
+
self,
|
| 414 |
+
repo: "Repo",
|
| 415 |
+
a_rawpath: Optional[bytes],
|
| 416 |
+
b_rawpath: Optional[bytes],
|
| 417 |
+
a_blob_id: Union[str, bytes, None],
|
| 418 |
+
b_blob_id: Union[str, bytes, None],
|
| 419 |
+
a_mode: Union[bytes, str, None],
|
| 420 |
+
b_mode: Union[bytes, str, None],
|
| 421 |
+
new_file: bool,
|
| 422 |
+
deleted_file: bool,
|
| 423 |
+
copied_file: bool,
|
| 424 |
+
raw_rename_from: Optional[bytes],
|
| 425 |
+
raw_rename_to: Optional[bytes],
|
| 426 |
+
diff: Union[str, bytes, None],
|
| 427 |
+
change_type: Optional[Lit_change_type],
|
| 428 |
+
score: Optional[int],
|
| 429 |
+
) -> None:
|
| 430 |
+
assert a_rawpath is None or isinstance(a_rawpath, bytes)
|
| 431 |
+
assert b_rawpath is None or isinstance(b_rawpath, bytes)
|
| 432 |
+
self.a_rawpath = a_rawpath
|
| 433 |
+
self.b_rawpath = b_rawpath
|
| 434 |
+
|
| 435 |
+
self.a_mode = mode_str_to_int(a_mode) if a_mode else None
|
| 436 |
+
self.b_mode = mode_str_to_int(b_mode) if b_mode else None
|
| 437 |
+
|
| 438 |
+
# Determine whether this diff references a submodule. If it does then
|
| 439 |
+
# we need to overwrite "repo" to the corresponding submodule's repo instead.
|
| 440 |
+
if repo and a_rawpath:
|
| 441 |
+
for submodule in repo.submodules:
|
| 442 |
+
if submodule.path == a_rawpath.decode(defenc, "replace"):
|
| 443 |
+
if submodule.module_exists():
|
| 444 |
+
repo = submodule.module()
|
| 445 |
+
break
|
| 446 |
+
|
| 447 |
+
self.a_blob: Union["IndexObject", None]
|
| 448 |
+
if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:
|
| 449 |
+
self.a_blob = None
|
| 450 |
+
else:
|
| 451 |
+
self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path)
|
| 452 |
+
|
| 453 |
+
self.b_blob: Union["IndexObject", None]
|
| 454 |
+
if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA:
|
| 455 |
+
self.b_blob = None
|
| 456 |
+
else:
|
| 457 |
+
self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path)
|
| 458 |
+
|
| 459 |
+
self.new_file: bool = new_file
|
| 460 |
+
self.deleted_file: bool = deleted_file
|
| 461 |
+
self.copied_file: bool = copied_file
|
| 462 |
+
|
| 463 |
+
# Be clear and use None instead of empty strings.
|
| 464 |
+
assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
|
| 465 |
+
assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
|
| 466 |
+
self.raw_rename_from = raw_rename_from or None
|
| 467 |
+
self.raw_rename_to = raw_rename_to or None
|
| 468 |
+
|
| 469 |
+
self.diff = diff
|
| 470 |
+
self.change_type: Union[Lit_change_type, None] = change_type
|
| 471 |
+
self.score = score
|
| 472 |
+
|
| 473 |
+
def __eq__(self, other: object) -> bool:
|
| 474 |
+
for name in self.__slots__:
|
| 475 |
+
if getattr(self, name) != getattr(other, name):
|
| 476 |
+
return False
|
| 477 |
+
# END for each name
|
| 478 |
+
return True
|
| 479 |
+
|
| 480 |
+
def __ne__(self, other: object) -> bool:
|
| 481 |
+
return not (self == other)
|
| 482 |
+
|
| 483 |
+
def __hash__(self) -> int:
|
| 484 |
+
return hash(tuple(getattr(self, n) for n in self.__slots__))
|
| 485 |
+
|
| 486 |
+
def __str__(self) -> str:
|
| 487 |
+
h = "%s"
|
| 488 |
+
if self.a_blob:
|
| 489 |
+
h %= self.a_blob.path
|
| 490 |
+
elif self.b_blob:
|
| 491 |
+
h %= self.b_blob.path
|
| 492 |
+
|
| 493 |
+
msg = ""
|
| 494 |
+
line = None
|
| 495 |
+
line_length = 0
|
| 496 |
+
for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")):
|
| 497 |
+
if b:
|
| 498 |
+
line = "\n%s: %o | %s" % (n, b.mode, b.hexsha)
|
| 499 |
+
else:
|
| 500 |
+
line = "\n%s: None" % n
|
| 501 |
+
# END if blob is not None
|
| 502 |
+
line_length = max(len(line), line_length)
|
| 503 |
+
msg += line
|
| 504 |
+
# END for each blob
|
| 505 |
+
|
| 506 |
+
# Add headline.
|
| 507 |
+
h += "\n" + "=" * line_length
|
| 508 |
+
|
| 509 |
+
if self.deleted_file:
|
| 510 |
+
msg += "\nfile deleted in rhs"
|
| 511 |
+
if self.new_file:
|
| 512 |
+
msg += "\nfile added in rhs"
|
| 513 |
+
if self.copied_file:
|
| 514 |
+
msg += "\nfile %r copied from %r" % (self.b_path, self.a_path)
|
| 515 |
+
if self.rename_from:
|
| 516 |
+
msg += "\nfile renamed from %r" % self.rename_from
|
| 517 |
+
if self.rename_to:
|
| 518 |
+
msg += "\nfile renamed to %r" % self.rename_to
|
| 519 |
+
if self.diff:
|
| 520 |
+
msg += "\n---"
|
| 521 |
+
try:
|
| 522 |
+
msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
|
| 523 |
+
except UnicodeDecodeError:
|
| 524 |
+
msg += "OMITTED BINARY DATA"
|
| 525 |
+
# END handle encoding
|
| 526 |
+
msg += "\n---"
|
| 527 |
+
# END diff info
|
| 528 |
+
|
| 529 |
+
return h + msg
|
| 530 |
+
|
| 531 |
+
@property
|
| 532 |
+
def a_path(self) -> Optional[str]:
|
| 533 |
+
return self.a_rawpath.decode(defenc, "replace") if self.a_rawpath else None
|
| 534 |
+
|
| 535 |
+
@property
|
| 536 |
+
def b_path(self) -> Optional[str]:
|
| 537 |
+
return self.b_rawpath.decode(defenc, "replace") if self.b_rawpath else None
|
| 538 |
+
|
| 539 |
+
@property
|
| 540 |
+
def rename_from(self) -> Optional[str]:
|
| 541 |
+
return self.raw_rename_from.decode(defenc, "replace") if self.raw_rename_from else None
|
| 542 |
+
|
| 543 |
+
@property
|
| 544 |
+
def rename_to(self) -> Optional[str]:
|
| 545 |
+
return self.raw_rename_to.decode(defenc, "replace") if self.raw_rename_to else None
|
| 546 |
+
|
| 547 |
+
@property
|
| 548 |
+
def renamed(self) -> bool:
|
| 549 |
+
"""Deprecated, use :attr:`renamed_file` instead.
|
| 550 |
+
|
| 551 |
+
:return:
|
| 552 |
+
``True`` if the blob of our diff has been renamed
|
| 553 |
+
|
| 554 |
+
:note:
|
| 555 |
+
This property is deprecated.
|
| 556 |
+
Please use the :attr:`renamed_file` property instead.
|
| 557 |
+
"""
|
| 558 |
+
warnings.warn(
|
| 559 |
+
"Diff.renamed is deprecated, use Diff.renamed_file instead",
|
| 560 |
+
DeprecationWarning,
|
| 561 |
+
stacklevel=2,
|
| 562 |
+
)
|
| 563 |
+
return self.renamed_file
|
| 564 |
+
|
| 565 |
+
@property
|
| 566 |
+
def renamed_file(self) -> bool:
|
| 567 |
+
""":return: ``True`` if the blob of our diff has been renamed"""
|
| 568 |
+
return self.rename_from != self.rename_to
|
| 569 |
+
|
| 570 |
+
@classmethod
|
| 571 |
+
def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
|
| 572 |
+
if path_match:
|
| 573 |
+
return decode_path(path_match)
|
| 574 |
+
|
| 575 |
+
if rename_match:
|
| 576 |
+
return decode_path(rename_match, has_ab_prefix=False)
|
| 577 |
+
|
| 578 |
+
if path_fallback_match:
|
| 579 |
+
return decode_path(path_fallback_match)
|
| 580 |
+
|
| 581 |
+
return None
|
| 582 |
+
|
| 583 |
+
@classmethod
|
| 584 |
+
def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
|
| 585 |
+
"""Create a new :class:`DiffIndex` from the given process output which must be
|
| 586 |
+
in patch format.
|
| 587 |
+
|
| 588 |
+
:param repo:
|
| 589 |
+
The repository we are operating on.
|
| 590 |
+
|
| 591 |
+
:param proc:
|
| 592 |
+
:manpage:`git-diff(1)` process to read from
|
| 593 |
+
(supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
|
| 594 |
+
|
| 595 |
+
:return:
|
| 596 |
+
:class:`DiffIndex`
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
# FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
|
| 600 |
+
text_list: List[bytes] = []
|
| 601 |
+
handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)
|
| 602 |
+
|
| 603 |
+
# For now, we have to bake the stream.
|
| 604 |
+
text = b"".join(text_list)
|
| 605 |
+
index: "DiffIndex" = DiffIndex()
|
| 606 |
+
previous_header: Union[Match[bytes], None] = None
|
| 607 |
+
header: Union[Match[bytes], None] = None
|
| 608 |
+
a_path, b_path = None, None # For mypy.
|
| 609 |
+
a_mode, b_mode = None, None # For mypy.
|
| 610 |
+
for _header in cls.re_header.finditer(text):
|
| 611 |
+
(
|
| 612 |
+
a_path_fallback,
|
| 613 |
+
b_path_fallback,
|
| 614 |
+
old_mode,
|
| 615 |
+
new_mode,
|
| 616 |
+
rename_from,
|
| 617 |
+
rename_to,
|
| 618 |
+
new_file_mode,
|
| 619 |
+
deleted_file_mode,
|
| 620 |
+
copied_file_name,
|
| 621 |
+
a_blob_id,
|
| 622 |
+
b_blob_id,
|
| 623 |
+
b_mode,
|
| 624 |
+
a_path,
|
| 625 |
+
b_path,
|
| 626 |
+
) = _header.groups()
|
| 627 |
+
|
| 628 |
+
new_file, deleted_file, copied_file = (
|
| 629 |
+
bool(new_file_mode),
|
| 630 |
+
bool(deleted_file_mode),
|
| 631 |
+
bool(copied_file_name),
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
|
| 635 |
+
b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
|
| 636 |
+
|
| 637 |
+
# Our only means to find the actual text is to see what has not been matched
|
| 638 |
+
# by our regex, and then retro-actively assign it to our index.
|
| 639 |
+
if previous_header is not None:
|
| 640 |
+
index[-1].diff = text[previous_header.end() : _header.start()]
|
| 641 |
+
# END assign actual diff
|
| 642 |
+
|
| 643 |
+
# Make sure the mode is set if the path is set. Otherwise the resulting blob
|
| 644 |
+
# is invalid. We just use the one mode we should have parsed.
|
| 645 |
+
a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
|
| 646 |
+
b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
|
| 647 |
+
index.append(
|
| 648 |
+
Diff(
|
| 649 |
+
repo,
|
| 650 |
+
a_path,
|
| 651 |
+
b_path,
|
| 652 |
+
a_blob_id and a_blob_id.decode(defenc),
|
| 653 |
+
b_blob_id and b_blob_id.decode(defenc),
|
| 654 |
+
a_mode and a_mode.decode(defenc),
|
| 655 |
+
b_mode and b_mode.decode(defenc),
|
| 656 |
+
new_file,
|
| 657 |
+
deleted_file,
|
| 658 |
+
copied_file,
|
| 659 |
+
rename_from,
|
| 660 |
+
rename_to,
|
| 661 |
+
None,
|
| 662 |
+
None,
|
| 663 |
+
None,
|
| 664 |
+
)
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
previous_header = _header
|
| 668 |
+
header = _header
|
| 669 |
+
# END for each header we parse
|
| 670 |
+
if index and header:
|
| 671 |
+
index[-1].diff = text[header.end() :]
|
| 672 |
+
# END assign last diff
|
| 673 |
+
|
| 674 |
+
return index
|
| 675 |
+
|
| 676 |
+
@staticmethod
|
| 677 |
+
def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
|
| 678 |
+
lines = lines_bytes.decode(defenc)
|
| 679 |
+
|
| 680 |
+
# Discard everything before the first colon, and the colon itself.
|
| 681 |
+
_, _, lines = lines.partition(":")
|
| 682 |
+
|
| 683 |
+
for line in lines.split("\x00:"):
|
| 684 |
+
if not line:
|
| 685 |
+
# The line data is empty, skip.
|
| 686 |
+
continue
|
| 687 |
+
meta, _, path = line.partition("\x00")
|
| 688 |
+
path = path.rstrip("\x00")
|
| 689 |
+
a_blob_id: Optional[str]
|
| 690 |
+
b_blob_id: Optional[str]
|
| 691 |
+
old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
|
| 692 |
+
# Change type can be R100
|
| 693 |
+
# R: status letter
|
| 694 |
+
# 100: score (in case of copy and rename)
|
| 695 |
+
change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
|
| 696 |
+
score_str = "".join(_change_type[1:])
|
| 697 |
+
score = int(score_str) if score_str.isdigit() else None
|
| 698 |
+
path = path.strip("\n")
|
| 699 |
+
a_path = path.encode(defenc)
|
| 700 |
+
b_path = path.encode(defenc)
|
| 701 |
+
deleted_file = False
|
| 702 |
+
new_file = False
|
| 703 |
+
copied_file = False
|
| 704 |
+
rename_from = None
|
| 705 |
+
rename_to = None
|
| 706 |
+
|
| 707 |
+
# NOTE: We cannot conclude from the existence of a blob to change type,
|
| 708 |
+
# as diffs with the working do not have blobs yet.
|
| 709 |
+
if change_type == "D":
|
| 710 |
+
b_blob_id = None # Optional[str]
|
| 711 |
+
deleted_file = True
|
| 712 |
+
elif change_type == "A":
|
| 713 |
+
a_blob_id = None
|
| 714 |
+
new_file = True
|
| 715 |
+
elif change_type == "C":
|
| 716 |
+
copied_file = True
|
| 717 |
+
a_path_str, b_path_str = path.split("\x00", 1)
|
| 718 |
+
a_path = a_path_str.encode(defenc)
|
| 719 |
+
b_path = b_path_str.encode(defenc)
|
| 720 |
+
elif change_type == "R":
|
| 721 |
+
a_path_str, b_path_str = path.split("\x00", 1)
|
| 722 |
+
a_path = a_path_str.encode(defenc)
|
| 723 |
+
b_path = b_path_str.encode(defenc)
|
| 724 |
+
rename_from, rename_to = a_path, b_path
|
| 725 |
+
elif change_type == "T":
|
| 726 |
+
# Nothing to do.
|
| 727 |
+
pass
|
| 728 |
+
# END add/remove handling
|
| 729 |
+
|
| 730 |
+
diff = Diff(
|
| 731 |
+
repo,
|
| 732 |
+
a_path,
|
| 733 |
+
b_path,
|
| 734 |
+
a_blob_id,
|
| 735 |
+
b_blob_id,
|
| 736 |
+
old_mode,
|
| 737 |
+
new_mode,
|
| 738 |
+
new_file,
|
| 739 |
+
deleted_file,
|
| 740 |
+
copied_file,
|
| 741 |
+
rename_from,
|
| 742 |
+
rename_to,
|
| 743 |
+
"",
|
| 744 |
+
change_type,
|
| 745 |
+
score,
|
| 746 |
+
)
|
| 747 |
+
index.append(diff)
|
| 748 |
+
|
| 749 |
+
@classmethod
|
| 750 |
+
def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
|
| 751 |
+
"""Create a new :class:`DiffIndex` from the given process output which must be
|
| 752 |
+
in raw format.
|
| 753 |
+
|
| 754 |
+
:param repo:
|
| 755 |
+
The repository we are operating on.
|
| 756 |
+
|
| 757 |
+
:param proc:
|
| 758 |
+
Process to read output from.
|
| 759 |
+
|
| 760 |
+
:return:
|
| 761 |
+
:class:`DiffIndex`
|
| 762 |
+
"""
|
| 763 |
+
# handles
|
| 764 |
+
# :100644 100644 687099101... 37c5e30c8... M .gitignore
|
| 765 |
+
|
| 766 |
+
index: "DiffIndex" = DiffIndex()
|
| 767 |
+
handle_process_output(
|
| 768 |
+
proc,
|
| 769 |
+
lambda byt: cls._handle_diff_line(byt, repo, index),
|
| 770 |
+
None,
|
| 771 |
+
finalize_process,
|
| 772 |
+
decode_streams=False,
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
return index
|
venv/lib/python3.10/site-packages/git/exc.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
"""Exceptions thrown throughout the git package."""
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
# Defined in gitdb.exc:
|
| 10 |
+
"AmbiguousObjectName",
|
| 11 |
+
"BadName",
|
| 12 |
+
"BadObject",
|
| 13 |
+
"BadObjectType",
|
| 14 |
+
"InvalidDBRoot",
|
| 15 |
+
"ODBError",
|
| 16 |
+
"ParseError",
|
| 17 |
+
"UnsupportedOperation",
|
| 18 |
+
# Introduced in this module:
|
| 19 |
+
"GitError",
|
| 20 |
+
"InvalidGitRepositoryError",
|
| 21 |
+
"WorkTreeRepositoryUnsupported",
|
| 22 |
+
"NoSuchPathError",
|
| 23 |
+
"UnsafeProtocolError",
|
| 24 |
+
"UnsafeOptionError",
|
| 25 |
+
"CommandError",
|
| 26 |
+
"GitCommandNotFound",
|
| 27 |
+
"GitCommandError",
|
| 28 |
+
"CheckoutError",
|
| 29 |
+
"CacheError",
|
| 30 |
+
"UnmergedEntriesError",
|
| 31 |
+
"HookExecutionError",
|
| 32 |
+
"RepositoryDirtyError",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
from gitdb.exc import (
|
| 36 |
+
AmbiguousObjectName,
|
| 37 |
+
BadName,
|
| 38 |
+
BadObject,
|
| 39 |
+
BadObjectType,
|
| 40 |
+
InvalidDBRoot,
|
| 41 |
+
ODBError,
|
| 42 |
+
ParseError,
|
| 43 |
+
UnsupportedOperation,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
from git.compat import safe_decode
|
| 47 |
+
from git.util import remove_password_if_present
|
| 48 |
+
|
| 49 |
+
# typing ----------------------------------------------------
|
| 50 |
+
|
| 51 |
+
from typing import List, Sequence, Tuple, TYPE_CHECKING, Union
|
| 52 |
+
|
| 53 |
+
from git.types import PathLike
|
| 54 |
+
|
| 55 |
+
if TYPE_CHECKING:
|
| 56 |
+
from git.repo.base import Repo
|
| 57 |
+
|
| 58 |
+
# ------------------------------------------------------------------
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class GitError(Exception):
|
| 62 |
+
"""Base class for all package exceptions."""
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class InvalidGitRepositoryError(GitError):
|
| 66 |
+
"""Thrown if the given repository appears to have an invalid format."""
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class WorkTreeRepositoryUnsupported(InvalidGitRepositoryError):
|
| 70 |
+
"""Thrown to indicate we can't handle work tree repositories."""
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class NoSuchPathError(GitError, OSError):
|
| 74 |
+
"""Thrown if a path could not be access by the system."""
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class UnsafeProtocolError(GitError):
|
| 78 |
+
"""Thrown if unsafe protocols are passed without being explicitly allowed."""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class UnsafeOptionError(GitError):
|
| 82 |
+
"""Thrown if unsafe options are passed without being explicitly allowed."""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class CommandError(GitError):
|
| 86 |
+
"""Base class for exceptions thrown at every stage of :class:`~subprocess.Popen`
|
| 87 |
+
execution.
|
| 88 |
+
|
| 89 |
+
:param command:
|
| 90 |
+
A non-empty list of argv comprising the command-line.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
_msg = "Cmd('%s') failed%s"
|
| 94 |
+
"""Format string with 2 ``%s`` for ``<cmdline>`` and the rest.
|
| 95 |
+
|
| 96 |
+
For example: ``"'%s' failed%s"``
|
| 97 |
+
|
| 98 |
+
Subclasses may override this attribute, provided it is still in this form.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(
|
| 102 |
+
self,
|
| 103 |
+
command: Union[List[str], Tuple[str, ...], str],
|
| 104 |
+
status: Union[str, int, None, Exception] = None,
|
| 105 |
+
stderr: Union[bytes, str, None] = None,
|
| 106 |
+
stdout: Union[bytes, str, None] = None,
|
| 107 |
+
) -> None:
|
| 108 |
+
if not isinstance(command, (tuple, list)):
|
| 109 |
+
command = command.split()
|
| 110 |
+
self.command = remove_password_if_present(command)
|
| 111 |
+
self.status = status
|
| 112 |
+
if status:
|
| 113 |
+
if isinstance(status, Exception):
|
| 114 |
+
status = "%s('%s')" % (type(status).__name__, safe_decode(str(status)))
|
| 115 |
+
else:
|
| 116 |
+
try:
|
| 117 |
+
status = "exit code(%s)" % int(status)
|
| 118 |
+
except (ValueError, TypeError):
|
| 119 |
+
s = safe_decode(str(status))
|
| 120 |
+
status = "'%s'" % s if isinstance(status, str) else s
|
| 121 |
+
|
| 122 |
+
self._cmd = safe_decode(self.command[0])
|
| 123 |
+
self._cmdline = " ".join(safe_decode(i) for i in self.command)
|
| 124 |
+
self._cause = status and " due to: %s" % status or "!"
|
| 125 |
+
stdout_decode = safe_decode(stdout)
|
| 126 |
+
stderr_decode = safe_decode(stderr)
|
| 127 |
+
self.stdout = stdout_decode and "\n stdout: '%s'" % stdout_decode or ""
|
| 128 |
+
self.stderr = stderr_decode and "\n stderr: '%s'" % stderr_decode or ""
|
| 129 |
+
|
| 130 |
+
def __str__(self) -> str:
|
| 131 |
+
return (self._msg + "\n cmdline: %s%s%s") % (
|
| 132 |
+
self._cmd,
|
| 133 |
+
self._cause,
|
| 134 |
+
self._cmdline,
|
| 135 |
+
self.stdout,
|
| 136 |
+
self.stderr,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class GitCommandNotFound(CommandError):
|
| 141 |
+
"""Thrown if we cannot find the ``git`` executable in the :envvar:`PATH` or at the
|
| 142 |
+
path given by the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable."""
|
| 143 |
+
|
| 144 |
+
def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
|
| 145 |
+
super().__init__(command, cause)
|
| 146 |
+
self._msg = "Cmd('%s') not found%s"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class GitCommandError(CommandError):
|
| 150 |
+
"""Thrown if execution of the git command fails with non-zero status code."""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
command: Union[List[str], Tuple[str, ...], str],
|
| 155 |
+
status: Union[str, int, None, Exception] = None,
|
| 156 |
+
stderr: Union[bytes, str, None] = None,
|
| 157 |
+
stdout: Union[bytes, str, None] = None,
|
| 158 |
+
) -> None:
|
| 159 |
+
super().__init__(command, status, stderr, stdout)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class CheckoutError(GitError):
|
| 163 |
+
"""Thrown if a file could not be checked out from the index as it contained
|
| 164 |
+
changes.
|
| 165 |
+
|
| 166 |
+
The :attr:`failed_files` attribute contains a list of relative paths that failed to
|
| 167 |
+
be checked out as they contained changes that did not exist in the index.
|
| 168 |
+
|
| 169 |
+
The :attr:`failed_reasons` attribute contains a string informing about the actual
|
| 170 |
+
cause of the issue.
|
| 171 |
+
|
| 172 |
+
The :attr:`valid_files` attribute contains a list of relative paths to files that
|
| 173 |
+
were checked out successfully and hence match the version stored in the index.
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
def __init__(
|
| 177 |
+
self,
|
| 178 |
+
message: str,
|
| 179 |
+
failed_files: Sequence[PathLike],
|
| 180 |
+
valid_files: Sequence[PathLike],
|
| 181 |
+
failed_reasons: List[str],
|
| 182 |
+
) -> None:
|
| 183 |
+
Exception.__init__(self, message)
|
| 184 |
+
self.failed_files = failed_files
|
| 185 |
+
self.failed_reasons = failed_reasons
|
| 186 |
+
self.valid_files = valid_files
|
| 187 |
+
|
| 188 |
+
def __str__(self) -> str:
|
| 189 |
+
return Exception.__str__(self) + ":%s" % self.failed_files
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class CacheError(GitError):
|
| 193 |
+
"""Base for all errors related to the git index, which is called "cache"
|
| 194 |
+
internally."""
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class UnmergedEntriesError(CacheError):
|
| 198 |
+
"""Thrown if an operation cannot proceed as there are still unmerged
|
| 199 |
+
entries in the cache."""
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class HookExecutionError(CommandError):
|
| 203 |
+
"""Thrown if a hook exits with a non-zero exit code.
|
| 204 |
+
|
| 205 |
+
This provides access to the exit code and the string returned via standard output.
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self,
|
| 210 |
+
command: Union[List[str], Tuple[str, ...], str],
|
| 211 |
+
status: Union[str, int, None, Exception],
|
| 212 |
+
stderr: Union[bytes, str, None] = None,
|
| 213 |
+
stdout: Union[bytes, str, None] = None,
|
| 214 |
+
) -> None:
|
| 215 |
+
super().__init__(command, status, stderr, stdout)
|
| 216 |
+
self._msg = "Hook('%s') failed%s"
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class RepositoryDirtyError(GitError):
|
| 220 |
+
"""Thrown whenever an operation on a repository fails as it has uncommitted changes
|
| 221 |
+
that would be overwritten."""
|
| 222 |
+
|
| 223 |
+
def __init__(self, repo: "Repo", message: str) -> None:
|
| 224 |
+
self.repo = repo
|
| 225 |
+
self.message = message
|
| 226 |
+
|
| 227 |
+
def __str__(self) -> str:
|
| 228 |
+
return "Operation cannot be performed on %r: %s" % (self.repo, self.message)
|
venv/lib/python3.10/site-packages/git/index/typ.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
"""Additional types used by the index."""
|
| 5 |
+
|
| 6 |
+
__all__ = ["BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType"]
|
| 7 |
+
|
| 8 |
+
from binascii import b2a_hex
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from git.objects import Blob
|
| 12 |
+
|
| 13 |
+
from .util import pack, unpack
|
| 14 |
+
|
| 15 |
+
# typing ----------------------------------------------------------------------
|
| 16 |
+
|
| 17 |
+
from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
|
| 18 |
+
|
| 19 |
+
from git.types import PathLike
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from git.repo import Repo
|
| 23 |
+
|
| 24 |
+
StageType = int
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
# { Invariants
|
| 29 |
+
CE_NAMEMASK = 0x0FFF
|
| 30 |
+
CE_STAGEMASK = 0x3000
|
| 31 |
+
CE_EXTENDED = 0x4000
|
| 32 |
+
CE_VALID = 0x8000
|
| 33 |
+
CE_STAGESHIFT = 12
|
| 34 |
+
|
| 35 |
+
# } END invariants
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class BlobFilter:
|
| 39 |
+
"""Predicate to be used by
|
| 40 |
+
:meth:`IndexFile.iter_blobs <git.index.base.IndexFile.iter_blobs>` allowing to
|
| 41 |
+
filter only return blobs which match the given list of directories or files.
|
| 42 |
+
|
| 43 |
+
The given paths are given relative to the repository.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
__slots__ = ("paths",)
|
| 47 |
+
|
| 48 |
+
def __init__(self, paths: Sequence[PathLike]) -> None:
|
| 49 |
+
"""
|
| 50 |
+
:param paths:
|
| 51 |
+
Tuple or list of paths which are either pointing to directories or to files
|
| 52 |
+
relative to the current repository.
|
| 53 |
+
"""
|
| 54 |
+
self.paths = paths
|
| 55 |
+
|
| 56 |
+
def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
|
| 57 |
+
blob_pathlike: PathLike = stage_blob[1].path
|
| 58 |
+
blob_path: Path = blob_pathlike if isinstance(blob_pathlike, Path) else Path(blob_pathlike)
|
| 59 |
+
for pathlike in self.paths:
|
| 60 |
+
path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike)
|
| 61 |
+
# TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no
|
| 62 |
+
# longer supported.
|
| 63 |
+
filter_parts = path.parts
|
| 64 |
+
blob_parts = blob_path.parts
|
| 65 |
+
if len(filter_parts) > len(blob_parts):
|
| 66 |
+
continue
|
| 67 |
+
if all(i == j for i, j in zip(filter_parts, blob_parts)):
|
| 68 |
+
return True
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class BaseIndexEntryHelper(NamedTuple):
|
| 73 |
+
"""Typed named tuple to provide named attribute access for :class:`BaseIndexEntry`.
|
| 74 |
+
|
| 75 |
+
This is needed to allow overriding ``__new__`` in child class to preserve backwards
|
| 76 |
+
compatibility.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
mode: int
|
| 80 |
+
binsha: bytes
|
| 81 |
+
flags: int
|
| 82 |
+
path: PathLike
|
| 83 |
+
ctime_bytes: bytes = pack(">LL", 0, 0)
|
| 84 |
+
mtime_bytes: bytes = pack(">LL", 0, 0)
|
| 85 |
+
dev: int = 0
|
| 86 |
+
inode: int = 0
|
| 87 |
+
uid: int = 0
|
| 88 |
+
gid: int = 0
|
| 89 |
+
size: int = 0
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class BaseIndexEntry(BaseIndexEntryHelper):
|
| 93 |
+
R"""Small brother of an index entry which can be created to describe changes
|
| 94 |
+
done to the index in which case plenty of additional information is not required.
|
| 95 |
+
|
| 96 |
+
As the first 4 data members match exactly to the :class:`IndexEntry` type, methods
|
| 97 |
+
expecting a :class:`BaseIndexEntry` can also handle full :class:`IndexEntry`\s even
|
| 98 |
+
if they use numeric indices for performance reasons.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __new__(
|
| 102 |
+
cls,
|
| 103 |
+
inp_tuple: Union[
|
| 104 |
+
Tuple[int, bytes, int, PathLike],
|
| 105 |
+
Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
|
| 106 |
+
],
|
| 107 |
+
) -> "BaseIndexEntry":
|
| 108 |
+
"""Override ``__new__`` to allow construction from a tuple for backwards
|
| 109 |
+
compatibility."""
|
| 110 |
+
return super().__new__(cls, *inp_tuple)
|
| 111 |
+
|
| 112 |
+
def __str__(self) -> str:
|
| 113 |
+
return "%o %s %i\t%s" % (self.mode, self.hexsha, self.stage, self.path)
|
| 114 |
+
|
| 115 |
+
def __repr__(self) -> str:
|
| 116 |
+
return "(%o, %s, %i, %s)" % (self.mode, self.hexsha, self.stage, self.path)
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def hexsha(self) -> str:
|
| 120 |
+
"""hex version of our sha"""
|
| 121 |
+
return b2a_hex(self.binsha).decode("ascii")
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def stage(self) -> int:
|
| 125 |
+
"""Stage of the entry, either:
|
| 126 |
+
|
| 127 |
+
* 0 = default stage
|
| 128 |
+
* 1 = stage before a merge or common ancestor entry in case of a 3 way merge
|
| 129 |
+
* 2 = stage of entries from the 'left' side of the merge
|
| 130 |
+
* 3 = stage of entries from the 'right' side of the merge
|
| 131 |
+
|
| 132 |
+
:note:
|
| 133 |
+
For more information, see :manpage:`git-read-tree(1)`.
|
| 134 |
+
"""
|
| 135 |
+
return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
|
| 136 |
+
|
| 137 |
+
@classmethod
|
| 138 |
+
def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry":
|
| 139 |
+
""":return: Fully equipped BaseIndexEntry at the given stage"""
|
| 140 |
+
return cls((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path))
|
| 141 |
+
|
| 142 |
+
def to_blob(self, repo: "Repo") -> Blob:
|
| 143 |
+
""":return: Blob using the information of this index entry"""
|
| 144 |
+
return Blob(repo, self.binsha, self.mode, self.path)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class IndexEntry(BaseIndexEntry):
|
| 148 |
+
"""Allows convenient access to index entry data as defined in
|
| 149 |
+
:class:`BaseIndexEntry` without completely unpacking it.
|
| 150 |
+
|
| 151 |
+
Attributes usually accessed often are cached in the tuple whereas others are
|
| 152 |
+
unpacked on demand.
|
| 153 |
+
|
| 154 |
+
See the properties for a mapping between names and tuple indices.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def ctime(self) -> Tuple[int, int]:
|
| 159 |
+
"""
|
| 160 |
+
:return:
|
| 161 |
+
Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
|
| 162 |
+
file's creation time
|
| 163 |
+
"""
|
| 164 |
+
return cast(Tuple[int, int], unpack(">LL", self.ctime_bytes))
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def mtime(self) -> Tuple[int, int]:
|
| 168 |
+
"""See :attr:`ctime` property, but returns modification time."""
|
| 169 |
+
return cast(Tuple[int, int], unpack(">LL", self.mtime_bytes))
|
| 170 |
+
|
| 171 |
+
@classmethod
|
| 172 |
+
def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry":
|
| 173 |
+
"""
|
| 174 |
+
:return:
|
| 175 |
+
Minimal entry as created from the given :class:`BaseIndexEntry` instance.
|
| 176 |
+
Missing values will be set to null-like values.
|
| 177 |
+
|
| 178 |
+
:param base:
|
| 179 |
+
Instance of type :class:`BaseIndexEntry`.
|
| 180 |
+
"""
|
| 181 |
+
time = pack(">LL", 0, 0)
|
| 182 |
+
return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0))
|
| 183 |
+
|
| 184 |
+
@classmethod
|
| 185 |
+
def from_blob(cls, blob: Blob, stage: int = 0) -> "IndexEntry":
|
| 186 |
+
""":return: Minimal entry resembling the given blob object"""
|
| 187 |
+
time = pack(">LL", 0, 0)
|
| 188 |
+
return IndexEntry(
|
| 189 |
+
(
|
| 190 |
+
blob.mode,
|
| 191 |
+
blob.binsha,
|
| 192 |
+
stage << CE_STAGESHIFT,
|
| 193 |
+
blob.path,
|
| 194 |
+
time,
|
| 195 |
+
time,
|
| 196 |
+
0,
|
| 197 |
+
0,
|
| 198 |
+
0,
|
| 199 |
+
0,
|
| 200 |
+
blob.size,
|
| 201 |
+
)
|
| 202 |
+
)
|
venv/lib/python3.10/site-packages/git/index/util.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
"""Index utilities."""
|
| 5 |
+
|
| 6 |
+
__all__ = ["TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir"]
|
| 7 |
+
|
| 8 |
+
import contextlib
|
| 9 |
+
from functools import wraps
|
| 10 |
+
import os
|
| 11 |
+
import os.path as osp
|
| 12 |
+
import struct
|
| 13 |
+
import tempfile
|
| 14 |
+
from types import TracebackType
|
| 15 |
+
|
| 16 |
+
# typing ----------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
from typing import Any, Callable, TYPE_CHECKING, Optional, Type
|
| 19 |
+
|
| 20 |
+
from git.types import Literal, PathLike, _T
|
| 21 |
+
|
| 22 |
+
if TYPE_CHECKING:
|
| 23 |
+
from git.index import IndexFile
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
# { Aliases
|
| 28 |
+
pack = struct.pack
|
| 29 |
+
unpack = struct.unpack
|
| 30 |
+
# } END aliases
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class TemporaryFileSwap:
|
| 34 |
+
"""Utility class moving a file to a temporary location within the same directory and
|
| 35 |
+
moving it back on to where on object deletion."""
|
| 36 |
+
|
| 37 |
+
__slots__ = ("file_path", "tmp_file_path")
|
| 38 |
+
|
| 39 |
+
def __init__(self, file_path: PathLike) -> None:
|
| 40 |
+
self.file_path = file_path
|
| 41 |
+
dirname, basename = osp.split(file_path)
|
| 42 |
+
fd, self.tmp_file_path = tempfile.mkstemp(prefix=basename, dir=dirname)
|
| 43 |
+
os.close(fd)
|
| 44 |
+
with contextlib.suppress(OSError): # It may be that the source does not exist.
|
| 45 |
+
os.replace(self.file_path, self.tmp_file_path)
|
| 46 |
+
|
| 47 |
+
def __enter__(self) -> "TemporaryFileSwap":
|
| 48 |
+
return self
|
| 49 |
+
|
| 50 |
+
def __exit__(
|
| 51 |
+
self,
|
| 52 |
+
exc_type: Optional[Type[BaseException]],
|
| 53 |
+
exc_val: Optional[BaseException],
|
| 54 |
+
exc_tb: Optional[TracebackType],
|
| 55 |
+
) -> Literal[False]:
|
| 56 |
+
if osp.isfile(self.tmp_file_path):
|
| 57 |
+
os.replace(self.tmp_file_path, self.file_path)
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# { Decorators
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
|
| 65 |
+
"""Decorator for functions that alter the index using the git command.
|
| 66 |
+
|
| 67 |
+
When a git command alters the index, this invalidates our possibly existing entries
|
| 68 |
+
dictionary, which is why it must be deleted to allow it to be lazily reread later.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
@wraps(func)
|
| 72 |
+
def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
|
| 73 |
+
rval = func(self, *args, **kwargs)
|
| 74 |
+
self._delete_entries_cache()
|
| 75 |
+
return rval
|
| 76 |
+
|
| 77 |
+
# END wrapper method
|
| 78 |
+
|
| 79 |
+
return post_clear_cache_if_not_raised
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def default_index(func: Callable[..., _T]) -> Callable[..., _T]:
|
| 83 |
+
"""Decorator ensuring the wrapped method may only run if we are the default
|
| 84 |
+
repository index.
|
| 85 |
+
|
| 86 |
+
This is as we rely on git commands that operate on that index only.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
@wraps(func)
|
| 90 |
+
def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
|
| 91 |
+
if self._file_path != self._index_path():
|
| 92 |
+
raise AssertionError(
|
| 93 |
+
"Cannot call %r on indices that do not represent the default git index" % func.__name__
|
| 94 |
+
)
|
| 95 |
+
return func(self, *args, **kwargs)
|
| 96 |
+
|
| 97 |
+
# END wrapper method
|
| 98 |
+
|
| 99 |
+
return check_default_index
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
|
| 103 |
+
"""Decorator which changes the current working dir to the one of the git
|
| 104 |
+
repository in order to ensure relative paths are handled correctly."""
|
| 105 |
+
|
| 106 |
+
@wraps(func)
|
| 107 |
+
def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
|
| 108 |
+
cur_wd = os.getcwd()
|
| 109 |
+
os.chdir(str(self.repo.working_tree_dir))
|
| 110 |
+
try:
|
| 111 |
+
return func(self, *args, **kwargs)
|
| 112 |
+
finally:
|
| 113 |
+
os.chdir(cur_wd)
|
| 114 |
+
# END handle working dir
|
| 115 |
+
|
| 116 |
+
# END wrapper
|
| 117 |
+
|
| 118 |
+
return set_git_working_dir
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# } END decorators
|
venv/lib/python3.10/site-packages/git/objects/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
"""Import all submodules' main classes into the package space."""
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"IndexObject",
|
| 8 |
+
"Object",
|
| 9 |
+
"Blob",
|
| 10 |
+
"Commit",
|
| 11 |
+
"Submodule",
|
| 12 |
+
"UpdateProgress",
|
| 13 |
+
"RootModule",
|
| 14 |
+
"RootUpdateProgress",
|
| 15 |
+
"TagObject",
|
| 16 |
+
"Tree",
|
| 17 |
+
"TreeModifier",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
from .base import IndexObject, Object
|
| 21 |
+
from .blob import Blob
|
| 22 |
+
from .commit import Commit
|
| 23 |
+
from .submodule import RootModule, RootUpdateProgress, Submodule, UpdateProgress
|
| 24 |
+
from .tag import TagObject
|
| 25 |
+
from .tree import Tree, TreeModifier
|
venv/lib/python3.10/site-packages/git/objects/__pycache__/base.cpython-310.pyc
ADDED
|
Binary file (9.92 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/__pycache__/fun.cpython-310.pyc
ADDED
|
Binary file (5.9 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/__pycache__/tag.cpython-310.pyc
ADDED
|
Binary file (3.61 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/__pycache__/tree.cpython-310.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/base.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
__all__ = ["Object", "IndexObject"]
|
| 7 |
+
|
| 8 |
+
import os.path as osp
|
| 9 |
+
|
| 10 |
+
import gitdb.typ as dbtyp
|
| 11 |
+
|
| 12 |
+
from git.exc import WorkTreeRepositoryUnsupported
|
| 13 |
+
from git.util import LazyMixin, bin_to_hex, join_path_native, stream_copy
|
| 14 |
+
|
| 15 |
+
from .util import get_object_type_by_name
|
| 16 |
+
|
| 17 |
+
# typing ------------------------------------------------------------------
|
| 18 |
+
|
| 19 |
+
from typing import Any, TYPE_CHECKING, Union
|
| 20 |
+
|
| 21 |
+
from git.types import AnyGitObject, GitObjectTypeString, PathLike
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from gitdb.base import OStream
|
| 25 |
+
|
| 26 |
+
from git.refs.reference import Reference
|
| 27 |
+
from git.repo import Repo
|
| 28 |
+
|
| 29 |
+
from .blob import Blob
|
| 30 |
+
from .submodule.base import Submodule
|
| 31 |
+
from .tree import Tree
|
| 32 |
+
|
| 33 |
+
IndexObjUnion = Union["Tree", "Blob", "Submodule"]
|
| 34 |
+
|
| 35 |
+
# --------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Object(LazyMixin):
|
| 39 |
+
"""Base class for classes representing git object types.
|
| 40 |
+
|
| 41 |
+
The following four leaf classes represent specific kinds of git objects:
|
| 42 |
+
|
| 43 |
+
* :class:`Blob <git.objects.blob.Blob>`
|
| 44 |
+
* :class:`Tree <git.objects.tree.Tree>`
|
| 45 |
+
* :class:`Commit <git.objects.commit.Commit>`
|
| 46 |
+
* :class:`TagObject <git.objects.tag.TagObject>`
|
| 47 |
+
|
| 48 |
+
See :manpage:`gitglossary(7)` on:
|
| 49 |
+
|
| 50 |
+
* "object": https://git-scm.com/docs/gitglossary#def_object
|
| 51 |
+
* "object type": https://git-scm.com/docs/gitglossary#def_object_type
|
| 52 |
+
* "blob": https://git-scm.com/docs/gitglossary#def_blob_object
|
| 53 |
+
* "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
|
| 54 |
+
* "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
|
| 55 |
+
* "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
|
| 56 |
+
|
| 57 |
+
:note:
|
| 58 |
+
See the :class:`~git.types.AnyGitObject` union type of the four leaf subclasses
|
| 59 |
+
that represent actual git object types.
|
| 60 |
+
|
| 61 |
+
:note:
|
| 62 |
+
:class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
|
| 63 |
+
rooted at this :class:`Object` class, even though submodules are not really a
|
| 64 |
+
type of git object. (This also applies to its
|
| 65 |
+
:class:`~git.objects.submodule.root.RootModule` subclass.)
|
| 66 |
+
|
| 67 |
+
:note:
|
| 68 |
+
This :class:`Object` class should not be confused with :class:`object` (the root
|
| 69 |
+
of the class hierarchy in Python).
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
NULL_HEX_SHA = "0" * 40
|
| 73 |
+
NULL_BIN_SHA = b"\0" * 20
|
| 74 |
+
|
| 75 |
+
TYPES = (
|
| 76 |
+
dbtyp.str_blob_type,
|
| 77 |
+
dbtyp.str_tree_type,
|
| 78 |
+
dbtyp.str_commit_type,
|
| 79 |
+
dbtyp.str_tag_type,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
__slots__ = ("repo", "binsha", "size")
|
| 83 |
+
|
| 84 |
+
type: Union[GitObjectTypeString, None] = None
|
| 85 |
+
"""String identifying (a concrete :class:`Object` subtype for) a git object type.
|
| 86 |
+
|
| 87 |
+
The subtypes that this may name correspond to the kinds of git objects that exist,
|
| 88 |
+
i.e., the objects that may be present in a git repository.
|
| 89 |
+
|
| 90 |
+
:note:
|
| 91 |
+
Most subclasses represent specific types of git objects and override this class
|
| 92 |
+
attribute accordingly. This attribute is ``None`` in the :class:`Object` base
|
| 93 |
+
class, as well as the :class:`IndexObject` intermediate subclass, but never
|
| 94 |
+
``None`` in concrete leaf subclasses representing specific git object types.
|
| 95 |
+
|
| 96 |
+
:note:
|
| 97 |
+
See also :class:`~git.types.GitObjectTypeString`.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def __init__(self, repo: "Repo", binsha: bytes) -> None:
|
| 101 |
+
"""Initialize an object by identifying it by its binary sha.
|
| 102 |
+
|
| 103 |
+
All keyword arguments will be set on demand if ``None``.
|
| 104 |
+
|
| 105 |
+
:param repo:
|
| 106 |
+
Repository this object is located in.
|
| 107 |
+
|
| 108 |
+
:param binsha:
|
| 109 |
+
20 byte SHA1
|
| 110 |
+
"""
|
| 111 |
+
super().__init__()
|
| 112 |
+
self.repo = repo
|
| 113 |
+
self.binsha = binsha
|
| 114 |
+
assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (
|
| 115 |
+
binsha,
|
| 116 |
+
len(binsha),
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
@classmethod
|
| 120 |
+
def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
|
| 121 |
+
"""
|
| 122 |
+
:return:
|
| 123 |
+
New :class:`Object` instance of a type appropriate to the object type behind
|
| 124 |
+
`id`. The id of the newly created object will be a binsha even though the
|
| 125 |
+
input id may have been a `~git.refs.reference.Reference` or rev-spec.
|
| 126 |
+
|
| 127 |
+
:param id:
|
| 128 |
+
:class:`~git.refs.reference.Reference`, rev-spec, or hexsha.
|
| 129 |
+
|
| 130 |
+
:note:
|
| 131 |
+
This cannot be a ``__new__`` method as it would always call :meth:`__init__`
|
| 132 |
+
with the input id which is not necessarily a binsha.
|
| 133 |
+
"""
|
| 134 |
+
return repo.rev_parse(str(id))
|
| 135 |
+
|
| 136 |
+
@classmethod
|
| 137 |
+
def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject:
|
| 138 |
+
"""
|
| 139 |
+
:return:
|
| 140 |
+
New object instance of a type appropriate to represent the given binary sha1
|
| 141 |
+
|
| 142 |
+
:param sha1:
|
| 143 |
+
20 byte binary sha1.
|
| 144 |
+
"""
|
| 145 |
+
if sha1 == cls.NULL_BIN_SHA:
|
| 146 |
+
# The NULL binsha is always the root commit.
|
| 147 |
+
return get_object_type_by_name(b"commit")(repo, sha1)
|
| 148 |
+
# END handle special case
|
| 149 |
+
oinfo = repo.odb.info(sha1)
|
| 150 |
+
inst = get_object_type_by_name(oinfo.type)(repo, oinfo.binsha)
|
| 151 |
+
inst.size = oinfo.size
|
| 152 |
+
return inst
|
| 153 |
+
|
| 154 |
+
def _set_cache_(self, attr: str) -> None:
|
| 155 |
+
"""Retrieve object information."""
|
| 156 |
+
if attr == "size":
|
| 157 |
+
oinfo = self.repo.odb.info(self.binsha)
|
| 158 |
+
self.size = oinfo.size # type: int
|
| 159 |
+
else:
|
| 160 |
+
super()._set_cache_(attr)
|
| 161 |
+
|
| 162 |
+
def __eq__(self, other: Any) -> bool:
|
| 163 |
+
""":return: ``True`` if the objects have the same SHA1"""
|
| 164 |
+
if not hasattr(other, "binsha"):
|
| 165 |
+
return False
|
| 166 |
+
return self.binsha == other.binsha
|
| 167 |
+
|
| 168 |
+
def __ne__(self, other: Any) -> bool:
|
| 169 |
+
""":return: ``True`` if the objects do not have the same SHA1"""
|
| 170 |
+
if not hasattr(other, "binsha"):
|
| 171 |
+
return True
|
| 172 |
+
return self.binsha != other.binsha
|
| 173 |
+
|
| 174 |
+
def __hash__(self) -> int:
|
| 175 |
+
""":return: Hash of our id allowing objects to be used in dicts and sets"""
|
| 176 |
+
return hash(self.binsha)
|
| 177 |
+
|
| 178 |
+
def __str__(self) -> str:
|
| 179 |
+
""":return: String of our SHA1 as understood by all git commands"""
|
| 180 |
+
return self.hexsha
|
| 181 |
+
|
| 182 |
+
def __repr__(self) -> str:
|
| 183 |
+
""":return: String with pythonic representation of our object"""
|
| 184 |
+
return '<git.%s "%s">' % (self.__class__.__name__, self.hexsha)
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def hexsha(self) -> str:
|
| 188 |
+
""":return: 40 byte hex version of our 20 byte binary sha"""
|
| 189 |
+
# b2a_hex produces bytes.
|
| 190 |
+
return bin_to_hex(self.binsha).decode("ascii")
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def data_stream(self) -> "OStream":
|
| 194 |
+
"""
|
| 195 |
+
:return:
|
| 196 |
+
File-object compatible stream to the uncompressed raw data of the object
|
| 197 |
+
|
| 198 |
+
:note:
|
| 199 |
+
Returned streams must be read in order.
|
| 200 |
+
"""
|
| 201 |
+
return self.repo.odb.stream(self.binsha)
|
| 202 |
+
|
| 203 |
+
def stream_data(self, ostream: "OStream") -> "Object":
|
| 204 |
+
"""Write our data directly to the given output stream.
|
| 205 |
+
|
| 206 |
+
:param ostream:
|
| 207 |
+
File-object compatible stream object.
|
| 208 |
+
|
| 209 |
+
:return:
|
| 210 |
+
self
|
| 211 |
+
"""
|
| 212 |
+
istream = self.repo.odb.stream(self.binsha)
|
| 213 |
+
stream_copy(istream, ostream)
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class IndexObject(Object):
|
| 218 |
+
"""Base for all objects that can be part of the index file.
|
| 219 |
+
|
| 220 |
+
The classes representing git object types that can be part of the index file are
|
| 221 |
+
:class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition,
|
| 222 |
+
:class:`~git.objects.submodule.base.Submodule`, which is not really a git object
|
| 223 |
+
type but can be part of an index file, is also a subclass.
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
__slots__ = ("path", "mode")
|
| 227 |
+
|
| 228 |
+
# For compatibility with iterable lists.
|
| 229 |
+
_id_attribute_ = "path"
|
| 230 |
+
|
| 231 |
+
def __init__(
|
| 232 |
+
self,
|
| 233 |
+
repo: "Repo",
|
| 234 |
+
binsha: bytes,
|
| 235 |
+
mode: Union[None, int] = None,
|
| 236 |
+
path: Union[None, PathLike] = None,
|
| 237 |
+
) -> None:
|
| 238 |
+
"""Initialize a newly instanced :class:`IndexObject`.
|
| 239 |
+
|
| 240 |
+
:param repo:
|
| 241 |
+
The :class:`~git.repo.base.Repo` we are located in.
|
| 242 |
+
|
| 243 |
+
:param binsha:
|
| 244 |
+
20 byte sha1.
|
| 245 |
+
|
| 246 |
+
:param mode:
|
| 247 |
+
The stat-compatible file mode as :class:`int`.
|
| 248 |
+
Use the :mod:`stat` module to evaluate the information.
|
| 249 |
+
|
| 250 |
+
:param path:
|
| 251 |
+
The path to the file in the file system, relative to the git repository
|
| 252 |
+
root, like ``file.ext`` or ``folder/other.ext``.
|
| 253 |
+
|
| 254 |
+
:note:
|
| 255 |
+
Path may not be set if the index object has been created directly, as it
|
| 256 |
+
cannot be retrieved without knowing the parent tree.
|
| 257 |
+
"""
|
| 258 |
+
super().__init__(repo, binsha)
|
| 259 |
+
if mode is not None:
|
| 260 |
+
self.mode = mode
|
| 261 |
+
if path is not None:
|
| 262 |
+
self.path = path
|
| 263 |
+
|
| 264 |
+
def __hash__(self) -> int:
|
| 265 |
+
"""
|
| 266 |
+
:return:
|
| 267 |
+
Hash of our path as index items are uniquely identifiable by path, not by
|
| 268 |
+
their data!
|
| 269 |
+
"""
|
| 270 |
+
return hash(self.path)
|
| 271 |
+
|
| 272 |
+
def _set_cache_(self, attr: str) -> None:
|
| 273 |
+
if attr in IndexObject.__slots__:
|
| 274 |
+
# They cannot be retrieved later on (not without searching for them).
|
| 275 |
+
raise AttributeError(
|
| 276 |
+
"Attribute '%s' unset: path and mode attributes must have been set during %s object creation"
|
| 277 |
+
% (attr, type(self).__name__)
|
| 278 |
+
)
|
| 279 |
+
else:
|
| 280 |
+
super()._set_cache_(attr)
|
| 281 |
+
# END handle slot attribute
|
| 282 |
+
|
| 283 |
+
@property
|
| 284 |
+
def name(self) -> str:
|
| 285 |
+
""":return: Name portion of the path, effectively being the basename"""
|
| 286 |
+
return osp.basename(self.path)
|
| 287 |
+
|
| 288 |
+
@property
|
| 289 |
+
def abspath(self) -> PathLike:
|
| 290 |
+
R"""
|
| 291 |
+
:return:
|
| 292 |
+
Absolute path to this index object in the file system (as opposed to the
|
| 293 |
+
:attr:`path` field which is a path relative to the git repository).
|
| 294 |
+
|
| 295 |
+
The returned path will be native to the system and contains ``\`` on
|
| 296 |
+
Windows.
|
| 297 |
+
"""
|
| 298 |
+
if self.repo.working_tree_dir is not None:
|
| 299 |
+
return join_path_native(self.repo.working_tree_dir, self.path)
|
| 300 |
+
else:
|
| 301 |
+
raise WorkTreeRepositoryUnsupported("working_tree_dir was None or empty")
|
venv/lib/python3.10/site-packages/git/objects/blob.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
__all__ = ["Blob"]
|
| 7 |
+
|
| 8 |
+
from mimetypes import guess_type
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
if sys.version_info >= (3, 8):
|
| 12 |
+
from typing import Literal
|
| 13 |
+
else:
|
| 14 |
+
from typing_extensions import Literal
|
| 15 |
+
|
| 16 |
+
from . import base
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Blob(base.IndexObject):
|
| 20 |
+
"""A Blob encapsulates a git blob object.
|
| 21 |
+
|
| 22 |
+
See :manpage:`gitglossary(7)` on "blob":
|
| 23 |
+
https://git-scm.com/docs/gitglossary#def_blob_object
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
DEFAULT_MIME_TYPE = "text/plain"
|
| 27 |
+
type: Literal["blob"] = "blob"
|
| 28 |
+
|
| 29 |
+
# Valid blob modes
|
| 30 |
+
executable_mode = 0o100755
|
| 31 |
+
file_mode = 0o100644
|
| 32 |
+
link_mode = 0o120000
|
| 33 |
+
|
| 34 |
+
__slots__ = ()
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def mime_type(self) -> str:
|
| 38 |
+
"""
|
| 39 |
+
:return:
|
| 40 |
+
String describing the mime type of this file (based on the filename)
|
| 41 |
+
|
| 42 |
+
:note:
|
| 43 |
+
Defaults to ``text/plain`` in case the actual file type is unknown.
|
| 44 |
+
"""
|
| 45 |
+
guesses = None
|
| 46 |
+
if self.path:
|
| 47 |
+
guesses = guess_type(str(self.path))
|
| 48 |
+
return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
|
venv/lib/python3.10/site-packages/git/objects/commit.py
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
__all__ = ["Commit"]
|
| 7 |
+
|
| 8 |
+
from collections import defaultdict
|
| 9 |
+
import datetime
|
| 10 |
+
from io import BytesIO
|
| 11 |
+
import logging
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
from subprocess import Popen, PIPE
|
| 15 |
+
import sys
|
| 16 |
+
from time import altzone, daylight, localtime, time, timezone
|
| 17 |
+
import warnings
|
| 18 |
+
|
| 19 |
+
from gitdb import IStream
|
| 20 |
+
|
| 21 |
+
from git.cmd import Git
|
| 22 |
+
from git.diff import Diffable
|
| 23 |
+
from git.util import Actor, Stats, finalize_process, hex_to_bin
|
| 24 |
+
|
| 25 |
+
from . import base
|
| 26 |
+
from .tree import Tree
|
| 27 |
+
from .util import (
|
| 28 |
+
Serializable,
|
| 29 |
+
TraversableIterableObj,
|
| 30 |
+
altz_to_utctz_str,
|
| 31 |
+
from_timestamp,
|
| 32 |
+
parse_actor_and_date,
|
| 33 |
+
parse_date,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# typing ------------------------------------------------------------------
|
| 37 |
+
|
| 38 |
+
from typing import (
|
| 39 |
+
Any,
|
| 40 |
+
Dict,
|
| 41 |
+
IO,
|
| 42 |
+
Iterator,
|
| 43 |
+
List,
|
| 44 |
+
Sequence,
|
| 45 |
+
Tuple,
|
| 46 |
+
TYPE_CHECKING,
|
| 47 |
+
Union,
|
| 48 |
+
cast,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
if sys.version_info >= (3, 8):
|
| 52 |
+
from typing import Literal
|
| 53 |
+
else:
|
| 54 |
+
from typing_extensions import Literal
|
| 55 |
+
|
| 56 |
+
from git.types import PathLike
|
| 57 |
+
|
| 58 |
+
if TYPE_CHECKING:
|
| 59 |
+
from git.refs import SymbolicReference
|
| 60 |
+
from git.repo import Repo
|
| 61 |
+
|
| 62 |
+
# ------------------------------------------------------------------------
|
| 63 |
+
|
| 64 |
+
_logger = logging.getLogger(__name__)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
|
| 68 |
+
"""Wraps a git commit object.
|
| 69 |
+
|
| 70 |
+
See :manpage:`gitglossary(7)` on "commit object":
|
| 71 |
+
https://git-scm.com/docs/gitglossary#def_commit_object
|
| 72 |
+
|
| 73 |
+
:note:
|
| 74 |
+
This class will act lazily on some of its attributes and will query the value on
|
| 75 |
+
demand only if it involves calling the git binary.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
# ENVIRONMENT VARIABLES
|
| 79 |
+
# Read when creating new commits.
|
| 80 |
+
env_author_date = "GIT_AUTHOR_DATE"
|
| 81 |
+
env_committer_date = "GIT_COMMITTER_DATE"
|
| 82 |
+
|
| 83 |
+
# CONFIGURATION KEYS
|
| 84 |
+
conf_encoding = "i18n.commitencoding"
|
| 85 |
+
|
| 86 |
+
# INVARIANTS
|
| 87 |
+
default_encoding = "UTF-8"
|
| 88 |
+
|
| 89 |
+
type: Literal["commit"] = "commit"
|
| 90 |
+
|
| 91 |
+
__slots__ = (
|
| 92 |
+
"tree",
|
| 93 |
+
"author",
|
| 94 |
+
"authored_date",
|
| 95 |
+
"author_tz_offset",
|
| 96 |
+
"committer",
|
| 97 |
+
"committed_date",
|
| 98 |
+
"committer_tz_offset",
|
| 99 |
+
"message",
|
| 100 |
+
"parents",
|
| 101 |
+
"encoding",
|
| 102 |
+
"gpgsig",
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
_id_attribute_ = "hexsha"
|
| 106 |
+
|
| 107 |
+
parents: Sequence["Commit"]
|
| 108 |
+
|
| 109 |
+
def __init__(
|
| 110 |
+
self,
|
| 111 |
+
repo: "Repo",
|
| 112 |
+
binsha: bytes,
|
| 113 |
+
tree: Union[Tree, None] = None,
|
| 114 |
+
author: Union[Actor, None] = None,
|
| 115 |
+
authored_date: Union[int, None] = None,
|
| 116 |
+
author_tz_offset: Union[None, float] = None,
|
| 117 |
+
committer: Union[Actor, None] = None,
|
| 118 |
+
committed_date: Union[int, None] = None,
|
| 119 |
+
committer_tz_offset: Union[None, float] = None,
|
| 120 |
+
message: Union[str, bytes, None] = None,
|
| 121 |
+
parents: Union[Sequence["Commit"], None] = None,
|
| 122 |
+
encoding: Union[str, None] = None,
|
| 123 |
+
gpgsig: Union[str, None] = None,
|
| 124 |
+
) -> None:
|
| 125 |
+
"""Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
|
| 126 |
+
default will be implicitly set on first query.
|
| 127 |
+
|
| 128 |
+
:param binsha:
|
| 129 |
+
20 byte sha1.
|
| 130 |
+
|
| 131 |
+
:param tree:
|
| 132 |
+
A :class:`~git.objects.tree.Tree` object.
|
| 133 |
+
|
| 134 |
+
:param author:
|
| 135 |
+
The author :class:`~git.util.Actor` object.
|
| 136 |
+
|
| 137 |
+
:param authored_date: int_seconds_since_epoch
|
| 138 |
+
The authored DateTime - use :func:`time.gmtime` to convert it into a
|
| 139 |
+
different format.
|
| 140 |
+
|
| 141 |
+
:param author_tz_offset: int_seconds_west_of_utc
|
| 142 |
+
The timezone that the `authored_date` is in.
|
| 143 |
+
|
| 144 |
+
:param committer:
|
| 145 |
+
The committer string, as an :class:`~git.util.Actor` object.
|
| 146 |
+
|
| 147 |
+
:param committed_date: int_seconds_since_epoch
|
| 148 |
+
The committed DateTime - use :func:`time.gmtime` to convert it into a
|
| 149 |
+
different format.
|
| 150 |
+
|
| 151 |
+
:param committer_tz_offset: int_seconds_west_of_utc
|
| 152 |
+
The timezone that the `committed_date` is in.
|
| 153 |
+
|
| 154 |
+
:param message: string
|
| 155 |
+
The commit message.
|
| 156 |
+
|
| 157 |
+
:param encoding: string
|
| 158 |
+
Encoding of the message, defaults to UTF-8.
|
| 159 |
+
|
| 160 |
+
:param parents:
|
| 161 |
+
List or tuple of :class:`Commit` objects which are our parent(s) in the
|
| 162 |
+
commit dependency graph.
|
| 163 |
+
|
| 164 |
+
:return:
|
| 165 |
+
:class:`Commit`
|
| 166 |
+
|
| 167 |
+
:note:
|
| 168 |
+
Timezone information is in the same format and in the same sign as what
|
| 169 |
+
:func:`time.altzone` returns. The sign is inverted compared to git's UTC
|
| 170 |
+
timezone.
|
| 171 |
+
"""
|
| 172 |
+
super().__init__(repo, binsha)
|
| 173 |
+
self.binsha = binsha
|
| 174 |
+
if tree is not None:
|
| 175 |
+
assert isinstance(tree, Tree), "Tree needs to be a Tree instance, was %s" % type(tree)
|
| 176 |
+
if tree is not None:
|
| 177 |
+
self.tree = tree
|
| 178 |
+
if author is not None:
|
| 179 |
+
self.author = author
|
| 180 |
+
if authored_date is not None:
|
| 181 |
+
self.authored_date = authored_date
|
| 182 |
+
if author_tz_offset is not None:
|
| 183 |
+
self.author_tz_offset = author_tz_offset
|
| 184 |
+
if committer is not None:
|
| 185 |
+
self.committer = committer
|
| 186 |
+
if committed_date is not None:
|
| 187 |
+
self.committed_date = committed_date
|
| 188 |
+
if committer_tz_offset is not None:
|
| 189 |
+
self.committer_tz_offset = committer_tz_offset
|
| 190 |
+
if message is not None:
|
| 191 |
+
self.message = message
|
| 192 |
+
if parents is not None:
|
| 193 |
+
self.parents = parents
|
| 194 |
+
if encoding is not None:
|
| 195 |
+
self.encoding = encoding
|
| 196 |
+
if gpgsig is not None:
|
| 197 |
+
self.gpgsig = gpgsig
|
| 198 |
+
|
| 199 |
+
@classmethod
|
| 200 |
+
def _get_intermediate_items(cls, commit: "Commit") -> Tuple["Commit", ...]:
|
| 201 |
+
return tuple(commit.parents)
|
| 202 |
+
|
| 203 |
+
@classmethod
|
| 204 |
+
def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes:
|
| 205 |
+
"""Calculate the sha of a commit.
|
| 206 |
+
|
| 207 |
+
:param repo:
|
| 208 |
+
:class:`~git.repo.base.Repo` object the commit should be part of.
|
| 209 |
+
|
| 210 |
+
:param commit:
|
| 211 |
+
:class:`Commit` object for which to generate the sha.
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
stream = BytesIO()
|
| 215 |
+
commit._serialize(stream)
|
| 216 |
+
streamlen = stream.tell()
|
| 217 |
+
stream.seek(0)
|
| 218 |
+
|
| 219 |
+
istream = repo.odb.store(IStream(cls.type, streamlen, stream))
|
| 220 |
+
return istream.binsha
|
| 221 |
+
|
| 222 |
+
def replace(self, **kwargs: Any) -> "Commit":
|
| 223 |
+
"""Create new commit object from an existing commit object.
|
| 224 |
+
|
| 225 |
+
Any values provided as keyword arguments will replace the corresponding
|
| 226 |
+
attribute in the new object.
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
attrs = {k: getattr(self, k) for k in self.__slots__}
|
| 230 |
+
|
| 231 |
+
for attrname in kwargs:
|
| 232 |
+
if attrname not in self.__slots__:
|
| 233 |
+
raise ValueError("invalid attribute name")
|
| 234 |
+
|
| 235 |
+
attrs.update(kwargs)
|
| 236 |
+
new_commit = self.__class__(self.repo, self.NULL_BIN_SHA, **attrs)
|
| 237 |
+
new_commit.binsha = self._calculate_sha_(self.repo, new_commit)
|
| 238 |
+
|
| 239 |
+
return new_commit
|
| 240 |
+
|
| 241 |
+
def _set_cache_(self, attr: str) -> None:
|
| 242 |
+
if attr in Commit.__slots__:
|
| 243 |
+
# Read the data in a chunk, its faster - then provide a file wrapper.
|
| 244 |
+
_binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha)
|
| 245 |
+
self._deserialize(BytesIO(stream.read()))
|
| 246 |
+
else:
|
| 247 |
+
super()._set_cache_(attr)
|
| 248 |
+
# END handle attrs
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def authored_datetime(self) -> datetime.datetime:
|
| 252 |
+
return from_timestamp(self.authored_date, self.author_tz_offset)
|
| 253 |
+
|
| 254 |
+
@property
|
| 255 |
+
def committed_datetime(self) -> datetime.datetime:
|
| 256 |
+
return from_timestamp(self.committed_date, self.committer_tz_offset)
|
| 257 |
+
|
| 258 |
+
@property
|
| 259 |
+
def summary(self) -> Union[str, bytes]:
|
| 260 |
+
""":return: First line of the commit message"""
|
| 261 |
+
if isinstance(self.message, str):
|
| 262 |
+
return self.message.split("\n", 1)[0]
|
| 263 |
+
else:
|
| 264 |
+
return self.message.split(b"\n", 1)[0]
|
| 265 |
+
|
| 266 |
+
def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> int:
|
| 267 |
+
"""Count the number of commits reachable from this commit.
|
| 268 |
+
|
| 269 |
+
:param paths:
|
| 270 |
+
An optional path or a list of paths restricting the return value to commits
|
| 271 |
+
actually containing the paths.
|
| 272 |
+
|
| 273 |
+
:param kwargs:
|
| 274 |
+
Additional options to be passed to :manpage:`git-rev-list(1)`. They must not
|
| 275 |
+
alter the output style of the command, or parsing will yield incorrect
|
| 276 |
+
results.
|
| 277 |
+
|
| 278 |
+
:return:
|
| 279 |
+
An int defining the number of reachable commits
|
| 280 |
+
"""
|
| 281 |
+
# Yes, it makes a difference whether empty paths are given or not in our case as
|
| 282 |
+
# the empty paths version will ignore merge commits for some reason.
|
| 283 |
+
if paths:
|
| 284 |
+
return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines())
|
| 285 |
+
return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines())
|
| 286 |
+
|
| 287 |
+
@property
|
| 288 |
+
def name_rev(self) -> str:
|
| 289 |
+
"""
|
| 290 |
+
:return:
|
| 291 |
+
String describing the commits hex sha based on the closest
|
| 292 |
+
`~git.refs.reference.Reference`.
|
| 293 |
+
|
| 294 |
+
:note:
|
| 295 |
+
Mostly useful for UI purposes.
|
| 296 |
+
"""
|
| 297 |
+
return self.repo.git.name_rev(self)
|
| 298 |
+
|
| 299 |
+
@classmethod
|
| 300 |
+
def iter_items(
|
| 301 |
+
cls,
|
| 302 |
+
repo: "Repo",
|
| 303 |
+
rev: Union[str, "Commit", "SymbolicReference"],
|
| 304 |
+
paths: Union[PathLike, Sequence[PathLike]] = "",
|
| 305 |
+
**kwargs: Any,
|
| 306 |
+
) -> Iterator["Commit"]:
|
| 307 |
+
R"""Find all commits matching the given criteria.
|
| 308 |
+
|
| 309 |
+
:param repo:
|
| 310 |
+
The :class:`~git.repo.base.Repo`.
|
| 311 |
+
|
| 312 |
+
:param rev:
|
| 313 |
+
Revision specifier. See :manpage:`git-rev-parse(1)` for viable options.
|
| 314 |
+
|
| 315 |
+
:param paths:
|
| 316 |
+
An optional path or list of paths. If set only :class:`Commit`\s that
|
| 317 |
+
include the path or paths will be considered.
|
| 318 |
+
|
| 319 |
+
:param kwargs:
|
| 320 |
+
Optional keyword arguments to :manpage:`git-rev-list(1)` where:
|
| 321 |
+
|
| 322 |
+
* ``max_count`` is the maximum number of commits to fetch.
|
| 323 |
+
* ``skip`` is the number of commits to skip.
|
| 324 |
+
* ``since`` selects all commits since some date, e.g. ``"1970-01-01"``.
|
| 325 |
+
|
| 326 |
+
:return:
|
| 327 |
+
Iterator yielding :class:`Commit` items.
|
| 328 |
+
"""
|
| 329 |
+
if "pretty" in kwargs:
|
| 330 |
+
raise ValueError("--pretty cannot be used as parsing expects single sha's only")
|
| 331 |
+
# END handle pretty
|
| 332 |
+
|
| 333 |
+
# Use -- in all cases, to prevent possibility of ambiguous arguments.
|
| 334 |
+
# See https://github.com/gitpython-developers/GitPython/issues/264.
|
| 335 |
+
|
| 336 |
+
args_list: List[PathLike] = ["--"]
|
| 337 |
+
|
| 338 |
+
if paths:
|
| 339 |
+
paths_tup: Tuple[PathLike, ...]
|
| 340 |
+
if isinstance(paths, (str, os.PathLike)):
|
| 341 |
+
paths_tup = (paths,)
|
| 342 |
+
else:
|
| 343 |
+
paths_tup = tuple(paths)
|
| 344 |
+
|
| 345 |
+
args_list.extend(paths_tup)
|
| 346 |
+
# END if paths
|
| 347 |
+
|
| 348 |
+
proc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs)
|
| 349 |
+
return cls._iter_from_process_or_stream(repo, proc)
|
| 350 |
+
|
| 351 |
+
def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> Iterator["Commit"]:
|
| 352 |
+
R"""Iterate _all_ parents of this commit.
|
| 353 |
+
|
| 354 |
+
:param paths:
|
| 355 |
+
Optional path or list of paths limiting the :class:`Commit`\s to those that
|
| 356 |
+
contain at least one of the paths.
|
| 357 |
+
|
| 358 |
+
:param kwargs:
|
| 359 |
+
All arguments allowed by :manpage:`git-rev-list(1)`.
|
| 360 |
+
|
| 361 |
+
:return:
|
| 362 |
+
Iterator yielding :class:`Commit` objects which are parents of ``self``
|
| 363 |
+
"""
|
| 364 |
+
# skip ourselves
|
| 365 |
+
skip = kwargs.get("skip", 1)
|
| 366 |
+
if skip == 0: # skip ourselves
|
| 367 |
+
skip = 1
|
| 368 |
+
kwargs["skip"] = skip
|
| 369 |
+
|
| 370 |
+
return self.iter_items(self.repo, self, paths, **kwargs)
|
| 371 |
+
|
| 372 |
+
@property
|
| 373 |
+
def stats(self) -> Stats:
|
| 374 |
+
"""Create a git stat from changes between this commit and its first parent
|
| 375 |
+
or from all changes done if this is the very first commit.
|
| 376 |
+
|
| 377 |
+
:return:
|
| 378 |
+
:class:`Stats`
|
| 379 |
+
"""
|
| 380 |
+
|
| 381 |
+
def process_lines(lines: List[str]) -> str:
|
| 382 |
+
text = ""
|
| 383 |
+
for file_info, line in zip(lines, lines[len(lines) // 2 :]):
|
| 384 |
+
change_type = file_info.split("\t")[0][-1]
|
| 385 |
+
(insertions, deletions, filename) = line.split("\t")
|
| 386 |
+
text += "%s\t%s\t%s\t%s\n" % (change_type, insertions, deletions, filename)
|
| 387 |
+
return text
|
| 388 |
+
|
| 389 |
+
if not self.parents:
|
| 390 |
+
lines = self.repo.git.diff_tree(
|
| 391 |
+
self.hexsha, "--", numstat=True, no_renames=True, root=True, raw=True
|
| 392 |
+
).splitlines()[1:]
|
| 393 |
+
text = process_lines(lines)
|
| 394 |
+
else:
|
| 395 |
+
lines = self.repo.git.diff(
|
| 396 |
+
self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True, raw=True
|
| 397 |
+
).splitlines()
|
| 398 |
+
text = process_lines(lines)
|
| 399 |
+
return Stats._list_from_string(self.repo, text)
|
| 400 |
+
|
| 401 |
+
@property
|
| 402 |
+
def trailers(self) -> Dict[str, str]:
|
| 403 |
+
"""Deprecated. Get the trailers of the message as a dictionary.
|
| 404 |
+
|
| 405 |
+
:note:
|
| 406 |
+
This property is deprecated, please use either :attr:`trailers_list` or
|
| 407 |
+
:attr:`trailers_dict`.
|
| 408 |
+
|
| 409 |
+
:return:
|
| 410 |
+
Dictionary containing whitespace stripped trailer information.
|
| 411 |
+
Only contains the latest instance of each trailer key.
|
| 412 |
+
"""
|
| 413 |
+
warnings.warn(
|
| 414 |
+
"Commit.trailers is deprecated, use Commit.trailers_list or Commit.trailers_dict instead",
|
| 415 |
+
DeprecationWarning,
|
| 416 |
+
stacklevel=2,
|
| 417 |
+
)
|
| 418 |
+
return {k: v[0] for k, v in self.trailers_dict.items()}
|
| 419 |
+
|
| 420 |
+
@property
|
| 421 |
+
def trailers_list(self) -> List[Tuple[str, str]]:
|
| 422 |
+
"""Get the trailers of the message as a list.
|
| 423 |
+
|
| 424 |
+
Git messages can contain trailer information that are similar to :rfc:`822`
|
| 425 |
+
e-mail headers. See :manpage:`git-interpret-trailers(1)`.
|
| 426 |
+
|
| 427 |
+
This function calls ``git interpret-trailers --parse`` onto the message to
|
| 428 |
+
extract the trailer information, returns the raw trailer data as a list.
|
| 429 |
+
|
| 430 |
+
Valid message with trailer::
|
| 431 |
+
|
| 432 |
+
Subject line
|
| 433 |
+
|
| 434 |
+
some body information
|
| 435 |
+
|
| 436 |
+
another information
|
| 437 |
+
|
| 438 |
+
key1: value1.1
|
| 439 |
+
key1: value1.2
|
| 440 |
+
key2 : value 2 with inner spaces
|
| 441 |
+
|
| 442 |
+
Returned list will look like this::
|
| 443 |
+
|
| 444 |
+
[
|
| 445 |
+
("key1", "value1.1"),
|
| 446 |
+
("key1", "value1.2"),
|
| 447 |
+
("key2", "value 2 with inner spaces"),
|
| 448 |
+
]
|
| 449 |
+
|
| 450 |
+
:return:
|
| 451 |
+
List containing key-value tuples of whitespace stripped trailer information.
|
| 452 |
+
"""
|
| 453 |
+
cmd = ["git", "interpret-trailers", "--parse"]
|
| 454 |
+
proc: Git.AutoInterrupt = self.repo.git.execute( # type: ignore[call-overload]
|
| 455 |
+
cmd,
|
| 456 |
+
as_process=True,
|
| 457 |
+
istream=PIPE,
|
| 458 |
+
)
|
| 459 |
+
trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
|
| 460 |
+
trailer = trailer.strip()
|
| 461 |
+
|
| 462 |
+
if not trailer:
|
| 463 |
+
return []
|
| 464 |
+
|
| 465 |
+
trailer_list = []
|
| 466 |
+
for t in trailer.split("\n"):
|
| 467 |
+
key, val = t.split(":", 1)
|
| 468 |
+
trailer_list.append((key.strip(), val.strip()))
|
| 469 |
+
|
| 470 |
+
return trailer_list
|
| 471 |
+
|
| 472 |
+
@property
|
| 473 |
+
def trailers_dict(self) -> Dict[str, List[str]]:
|
| 474 |
+
"""Get the trailers of the message as a dictionary.
|
| 475 |
+
|
| 476 |
+
Git messages can contain trailer information that are similar to :rfc:`822`
|
| 477 |
+
e-mail headers. See :manpage:`git-interpret-trailers(1)`.
|
| 478 |
+
|
| 479 |
+
This function calls ``git interpret-trailers --parse`` onto the message to
|
| 480 |
+
extract the trailer information. The key value pairs are stripped of leading and
|
| 481 |
+
trailing whitespaces before they get saved into a dictionary.
|
| 482 |
+
|
| 483 |
+
Valid message with trailer::
|
| 484 |
+
|
| 485 |
+
Subject line
|
| 486 |
+
|
| 487 |
+
some body information
|
| 488 |
+
|
| 489 |
+
another information
|
| 490 |
+
|
| 491 |
+
key1: value1.1
|
| 492 |
+
key1: value1.2
|
| 493 |
+
key2 : value 2 with inner spaces
|
| 494 |
+
|
| 495 |
+
Returned dictionary will look like this::
|
| 496 |
+
|
| 497 |
+
{
|
| 498 |
+
"key1": ["value1.1", "value1.2"],
|
| 499 |
+
"key2": ["value 2 with inner spaces"],
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
:return:
|
| 504 |
+
Dictionary containing whitespace stripped trailer information, mapping
|
| 505 |
+
trailer keys to a list of their corresponding values.
|
| 506 |
+
"""
|
| 507 |
+
d = defaultdict(list)
|
| 508 |
+
for key, val in self.trailers_list:
|
| 509 |
+
d[key].append(val)
|
| 510 |
+
return dict(d)
|
| 511 |
+
|
| 512 |
+
@classmethod
|
| 513 |
+
def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:
|
| 514 |
+
"""Parse out commit information into a list of :class:`Commit` objects.
|
| 515 |
+
|
| 516 |
+
We expect one line per commit, and parse the actual commit information directly
|
| 517 |
+
from our lighting fast object database.
|
| 518 |
+
|
| 519 |
+
:param proc:
|
| 520 |
+
:manpage:`git-rev-list(1)` process instance - one sha per line.
|
| 521 |
+
|
| 522 |
+
:return:
|
| 523 |
+
Iterator supplying :class:`Commit` objects
|
| 524 |
+
"""
|
| 525 |
+
|
| 526 |
+
# def is_proc(inp) -> TypeGuard[Popen]:
|
| 527 |
+
# return hasattr(proc_or_stream, 'wait') and not hasattr(proc_or_stream, 'readline')
|
| 528 |
+
|
| 529 |
+
# def is_stream(inp) -> TypeGuard[IO]:
|
| 530 |
+
# return hasattr(proc_or_stream, 'readline')
|
| 531 |
+
|
| 532 |
+
if hasattr(proc_or_stream, "wait"):
|
| 533 |
+
proc_or_stream = cast(Popen, proc_or_stream)
|
| 534 |
+
if proc_or_stream.stdout is not None:
|
| 535 |
+
stream = proc_or_stream.stdout
|
| 536 |
+
elif hasattr(proc_or_stream, "readline"):
|
| 537 |
+
proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast]
|
| 538 |
+
stream = proc_or_stream
|
| 539 |
+
|
| 540 |
+
readline = stream.readline
|
| 541 |
+
while True:
|
| 542 |
+
line = readline()
|
| 543 |
+
if not line:
|
| 544 |
+
break
|
| 545 |
+
hexsha = line.strip()
|
| 546 |
+
if len(hexsha) > 40:
|
| 547 |
+
# Split additional information, as returned by bisect for instance.
|
| 548 |
+
hexsha, _ = line.split(None, 1)
|
| 549 |
+
# END handle extra info
|
| 550 |
+
|
| 551 |
+
assert len(hexsha) == 40, "Invalid line: %s" % hexsha
|
| 552 |
+
yield cls(repo, hex_to_bin(hexsha))
|
| 553 |
+
# END for each line in stream
|
| 554 |
+
|
| 555 |
+
# TODO: Review this - it seems process handling got a bit out of control due to
|
| 556 |
+
# many developers trying to fix the open file handles issue.
|
| 557 |
+
if hasattr(proc_or_stream, "wait"):
|
| 558 |
+
proc_or_stream = cast(Popen, proc_or_stream)
|
| 559 |
+
finalize_process(proc_or_stream)
|
| 560 |
+
|
| 561 |
+
@classmethod
|
| 562 |
+
def create_from_tree(
|
| 563 |
+
cls,
|
| 564 |
+
repo: "Repo",
|
| 565 |
+
tree: Union[Tree, str],
|
| 566 |
+
message: str,
|
| 567 |
+
parent_commits: Union[None, List["Commit"]] = None,
|
| 568 |
+
head: bool = False,
|
| 569 |
+
author: Union[None, Actor] = None,
|
| 570 |
+
committer: Union[None, Actor] = None,
|
| 571 |
+
author_date: Union[None, str, datetime.datetime] = None,
|
| 572 |
+
commit_date: Union[None, str, datetime.datetime] = None,
|
| 573 |
+
) -> "Commit":
|
| 574 |
+
"""Commit the given tree, creating a :class:`Commit` object.
|
| 575 |
+
|
| 576 |
+
:param repo:
|
| 577 |
+
:class:`~git.repo.base.Repo` object the commit should be part of.
|
| 578 |
+
|
| 579 |
+
:param tree:
|
| 580 |
+
:class:`~git.objects.tree.Tree` object or hex or bin sha.
|
| 581 |
+
The tree of the new commit.
|
| 582 |
+
|
| 583 |
+
:param message:
|
| 584 |
+
Commit message. It may be an empty string if no message is provided. It will
|
| 585 |
+
be converted to a string, in any case.
|
| 586 |
+
|
| 587 |
+
:param parent_commits:
|
| 588 |
+
Optional :class:`Commit` objects to use as parents for the new commit. If
|
| 589 |
+
empty list, the commit will have no parents at all and become a root commit.
|
| 590 |
+
If ``None``, the current head commit will be the parent of the new commit
|
| 591 |
+
object.
|
| 592 |
+
|
| 593 |
+
:param head:
|
| 594 |
+
If ``True``, the HEAD will be advanced to the new commit automatically.
|
| 595 |
+
Otherwise the HEAD will remain pointing on the previous commit. This could
|
| 596 |
+
lead to undesired results when diffing files.
|
| 597 |
+
|
| 598 |
+
:param author:
|
| 599 |
+
The name of the author, optional.
|
| 600 |
+
If unset, the repository configuration is used to obtain this value.
|
| 601 |
+
|
| 602 |
+
:param committer:
|
| 603 |
+
The name of the committer, optional.
|
| 604 |
+
If unset, the repository configuration is used to obtain this value.
|
| 605 |
+
|
| 606 |
+
:param author_date:
|
| 607 |
+
The timestamp for the author field.
|
| 608 |
+
|
| 609 |
+
:param commit_date:
|
| 610 |
+
The timestamp for the committer field.
|
| 611 |
+
|
| 612 |
+
:return:
|
| 613 |
+
:class:`Commit` object representing the new commit.
|
| 614 |
+
|
| 615 |
+
:note:
|
| 616 |
+
Additional information about the committer and author are taken from the
|
| 617 |
+
environment or from the git configuration. See :manpage:`git-commit-tree(1)`
|
| 618 |
+
for more information.
|
| 619 |
+
"""
|
| 620 |
+
if parent_commits is None:
|
| 621 |
+
try:
|
| 622 |
+
parent_commits = [repo.head.commit]
|
| 623 |
+
except ValueError:
|
| 624 |
+
# Empty repositories have no head commit.
|
| 625 |
+
parent_commits = []
|
| 626 |
+
# END handle parent commits
|
| 627 |
+
else:
|
| 628 |
+
for p in parent_commits:
|
| 629 |
+
if not isinstance(p, cls):
|
| 630 |
+
raise ValueError(f"Parent commit '{p!r}' must be of type {cls}")
|
| 631 |
+
# END check parent commit types
|
| 632 |
+
# END if parent commits are unset
|
| 633 |
+
|
| 634 |
+
# Retrieve all additional information, create a commit object, and serialize it.
|
| 635 |
+
# Generally:
|
| 636 |
+
# * Environment variables override configuration values.
|
| 637 |
+
# * Sensible defaults are set according to the git documentation.
|
| 638 |
+
|
| 639 |
+
# COMMITTER AND AUTHOR INFO
|
| 640 |
+
cr = repo.config_reader()
|
| 641 |
+
env = os.environ
|
| 642 |
+
|
| 643 |
+
committer = committer or Actor.committer(cr)
|
| 644 |
+
author = author or Actor.author(cr)
|
| 645 |
+
|
| 646 |
+
# PARSE THE DATES
|
| 647 |
+
unix_time = int(time())
|
| 648 |
+
is_dst = daylight and localtime().tm_isdst > 0
|
| 649 |
+
offset = altzone if is_dst else timezone
|
| 650 |
+
|
| 651 |
+
author_date_str = env.get(cls.env_author_date, "")
|
| 652 |
+
if author_date:
|
| 653 |
+
author_time, author_offset = parse_date(author_date)
|
| 654 |
+
elif author_date_str:
|
| 655 |
+
author_time, author_offset = parse_date(author_date_str)
|
| 656 |
+
else:
|
| 657 |
+
author_time, author_offset = unix_time, offset
|
| 658 |
+
# END set author time
|
| 659 |
+
|
| 660 |
+
committer_date_str = env.get(cls.env_committer_date, "")
|
| 661 |
+
if commit_date:
|
| 662 |
+
committer_time, committer_offset = parse_date(commit_date)
|
| 663 |
+
elif committer_date_str:
|
| 664 |
+
committer_time, committer_offset = parse_date(committer_date_str)
|
| 665 |
+
else:
|
| 666 |
+
committer_time, committer_offset = unix_time, offset
|
| 667 |
+
# END set committer time
|
| 668 |
+
|
| 669 |
+
# Assume UTF-8 encoding.
|
| 670 |
+
enc_section, enc_option = cls.conf_encoding.split(".")
|
| 671 |
+
conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding)
|
| 672 |
+
if not isinstance(conf_encoding, str):
|
| 673 |
+
raise TypeError("conf_encoding could not be coerced to str")
|
| 674 |
+
|
| 675 |
+
# If the tree is no object, make sure we create one - otherwise the created
|
| 676 |
+
# commit object is invalid.
|
| 677 |
+
if isinstance(tree, str):
|
| 678 |
+
tree = repo.tree(tree)
|
| 679 |
+
# END tree conversion
|
| 680 |
+
|
| 681 |
+
# CREATE NEW COMMIT
|
| 682 |
+
new_commit = cls(
|
| 683 |
+
repo,
|
| 684 |
+
cls.NULL_BIN_SHA,
|
| 685 |
+
tree,
|
| 686 |
+
author,
|
| 687 |
+
author_time,
|
| 688 |
+
author_offset,
|
| 689 |
+
committer,
|
| 690 |
+
committer_time,
|
| 691 |
+
committer_offset,
|
| 692 |
+
message,
|
| 693 |
+
parent_commits,
|
| 694 |
+
conf_encoding,
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
new_commit.binsha = cls._calculate_sha_(repo, new_commit)
|
| 698 |
+
|
| 699 |
+
if head:
|
| 700 |
+
# Need late import here, importing git at the very beginning throws as
|
| 701 |
+
# well...
|
| 702 |
+
import git.refs
|
| 703 |
+
|
| 704 |
+
try:
|
| 705 |
+
repo.head.set_commit(new_commit, logmsg=message)
|
| 706 |
+
except ValueError:
|
| 707 |
+
# head is not yet set to the ref our HEAD points to.
|
| 708 |
+
# Happens on first commit.
|
| 709 |
+
master = git.refs.Head.create(
|
| 710 |
+
repo,
|
| 711 |
+
repo.head.ref,
|
| 712 |
+
new_commit,
|
| 713 |
+
logmsg="commit (initial): %s" % message,
|
| 714 |
+
)
|
| 715 |
+
repo.head.set_reference(master, logmsg="commit: Switching to %s" % master)
|
| 716 |
+
# END handle empty repositories
|
| 717 |
+
# END advance head handling
|
| 718 |
+
|
| 719 |
+
return new_commit
|
| 720 |
+
|
| 721 |
+
# { Serializable Implementation
|
| 722 |
+
|
| 723 |
+
def _serialize(self, stream: BytesIO) -> "Commit":
|
| 724 |
+
write = stream.write
|
| 725 |
+
write(("tree %s\n" % self.tree).encode("ascii"))
|
| 726 |
+
for p in self.parents:
|
| 727 |
+
write(("parent %s\n" % p).encode("ascii"))
|
| 728 |
+
|
| 729 |
+
a = self.author
|
| 730 |
+
aname = a.name
|
| 731 |
+
c = self.committer
|
| 732 |
+
fmt = "%s %s <%s> %s %s\n"
|
| 733 |
+
write(
|
| 734 |
+
(
|
| 735 |
+
fmt
|
| 736 |
+
% (
|
| 737 |
+
"author",
|
| 738 |
+
aname,
|
| 739 |
+
a.email,
|
| 740 |
+
self.authored_date,
|
| 741 |
+
altz_to_utctz_str(self.author_tz_offset),
|
| 742 |
+
)
|
| 743 |
+
).encode(self.encoding)
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
# Encode committer.
|
| 747 |
+
aname = c.name
|
| 748 |
+
write(
|
| 749 |
+
(
|
| 750 |
+
fmt
|
| 751 |
+
% (
|
| 752 |
+
"committer",
|
| 753 |
+
aname,
|
| 754 |
+
c.email,
|
| 755 |
+
self.committed_date,
|
| 756 |
+
altz_to_utctz_str(self.committer_tz_offset),
|
| 757 |
+
)
|
| 758 |
+
).encode(self.encoding)
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
if self.encoding != self.default_encoding:
|
| 762 |
+
write(("encoding %s\n" % self.encoding).encode("ascii"))
|
| 763 |
+
|
| 764 |
+
try:
|
| 765 |
+
if self.__getattribute__("gpgsig"):
|
| 766 |
+
write(b"gpgsig")
|
| 767 |
+
for sigline in self.gpgsig.rstrip("\n").split("\n"):
|
| 768 |
+
write((" " + sigline + "\n").encode("ascii"))
|
| 769 |
+
except AttributeError:
|
| 770 |
+
pass
|
| 771 |
+
|
| 772 |
+
write(b"\n")
|
| 773 |
+
|
| 774 |
+
# Write plain bytes, be sure its encoded according to our encoding.
|
| 775 |
+
if isinstance(self.message, str):
|
| 776 |
+
write(self.message.encode(self.encoding))
|
| 777 |
+
else:
|
| 778 |
+
write(self.message)
|
| 779 |
+
# END handle encoding
|
| 780 |
+
return self
|
| 781 |
+
|
| 782 |
+
def _deserialize(self, stream: BytesIO) -> "Commit":
|
| 783 |
+
readline = stream.readline
|
| 784 |
+
self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "")
|
| 785 |
+
|
| 786 |
+
self.parents = []
|
| 787 |
+
next_line = None
|
| 788 |
+
while True:
|
| 789 |
+
parent_line = readline()
|
| 790 |
+
if not parent_line.startswith(b"parent"):
|
| 791 |
+
next_line = parent_line
|
| 792 |
+
break
|
| 793 |
+
# END abort reading parents
|
| 794 |
+
self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode("ascii"))))
|
| 795 |
+
# END for each parent line
|
| 796 |
+
self.parents = tuple(self.parents)
|
| 797 |
+
|
| 798 |
+
# We don't know actual author encoding before we have parsed it, so keep the
|
| 799 |
+
# lines around.
|
| 800 |
+
author_line = next_line
|
| 801 |
+
committer_line = readline()
|
| 802 |
+
|
| 803 |
+
# We might run into one or more mergetag blocks, skip those for now.
|
| 804 |
+
next_line = readline()
|
| 805 |
+
while next_line.startswith(b"mergetag "):
|
| 806 |
+
next_line = readline()
|
| 807 |
+
while next_line.startswith(b" "):
|
| 808 |
+
next_line = readline()
|
| 809 |
+
# END skip mergetags
|
| 810 |
+
|
| 811 |
+
# Now we can have the encoding line, or an empty line followed by the optional
|
| 812 |
+
# message.
|
| 813 |
+
self.encoding = self.default_encoding
|
| 814 |
+
self.gpgsig = ""
|
| 815 |
+
|
| 816 |
+
# Read headers.
|
| 817 |
+
enc = next_line
|
| 818 |
+
buf = enc.strip()
|
| 819 |
+
while buf:
|
| 820 |
+
if buf[0:10] == b"encoding ":
|
| 821 |
+
self.encoding = buf[buf.find(b" ") + 1 :].decode(self.encoding, "ignore")
|
| 822 |
+
elif buf[0:7] == b"gpgsig ":
|
| 823 |
+
sig = buf[buf.find(b" ") + 1 :] + b"\n"
|
| 824 |
+
is_next_header = False
|
| 825 |
+
while True:
|
| 826 |
+
sigbuf = readline()
|
| 827 |
+
if not sigbuf:
|
| 828 |
+
break
|
| 829 |
+
if sigbuf[0:1] != b" ":
|
| 830 |
+
buf = sigbuf.strip()
|
| 831 |
+
is_next_header = True
|
| 832 |
+
break
|
| 833 |
+
sig += sigbuf[1:]
|
| 834 |
+
# END read all signature
|
| 835 |
+
self.gpgsig = sig.rstrip(b"\n").decode(self.encoding, "ignore")
|
| 836 |
+
if is_next_header:
|
| 837 |
+
continue
|
| 838 |
+
buf = readline().strip()
|
| 839 |
+
|
| 840 |
+
# Decode the author's name.
|
| 841 |
+
try:
|
| 842 |
+
(
|
| 843 |
+
self.author,
|
| 844 |
+
self.authored_date,
|
| 845 |
+
self.author_tz_offset,
|
| 846 |
+
) = parse_actor_and_date(author_line.decode(self.encoding, "replace"))
|
| 847 |
+
except UnicodeDecodeError:
|
| 848 |
+
_logger.error(
|
| 849 |
+
"Failed to decode author line '%s' using encoding %s",
|
| 850 |
+
author_line,
|
| 851 |
+
self.encoding,
|
| 852 |
+
exc_info=True,
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
try:
|
| 856 |
+
(
|
| 857 |
+
self.committer,
|
| 858 |
+
self.committed_date,
|
| 859 |
+
self.committer_tz_offset,
|
| 860 |
+
) = parse_actor_and_date(committer_line.decode(self.encoding, "replace"))
|
| 861 |
+
except UnicodeDecodeError:
|
| 862 |
+
_logger.error(
|
| 863 |
+
"Failed to decode committer line '%s' using encoding %s",
|
| 864 |
+
committer_line,
|
| 865 |
+
self.encoding,
|
| 866 |
+
exc_info=True,
|
| 867 |
+
)
|
| 868 |
+
# END handle author's encoding
|
| 869 |
+
|
| 870 |
+
# A stream from our data simply gives us the plain message.
|
| 871 |
+
# The end of our message stream is marked with a newline that we strip.
|
| 872 |
+
self.message = stream.read()
|
| 873 |
+
try:
|
| 874 |
+
self.message = self.message.decode(self.encoding, "replace")
|
| 875 |
+
except UnicodeDecodeError:
|
| 876 |
+
_logger.error(
|
| 877 |
+
"Failed to decode message '%s' using encoding %s",
|
| 878 |
+
self.message,
|
| 879 |
+
self.encoding,
|
| 880 |
+
exc_info=True,
|
| 881 |
+
)
|
| 882 |
+
# END exception handling
|
| 883 |
+
|
| 884 |
+
return self
|
| 885 |
+
|
| 886 |
+
# } END serializable implementation
|
| 887 |
+
|
| 888 |
+
@property
|
| 889 |
+
def co_authors(self) -> List[Actor]:
|
| 890 |
+
"""Search the commit message for any co-authors of this commit.
|
| 891 |
+
|
| 892 |
+
Details on co-authors:
|
| 893 |
+
https://github.blog/2018-01-29-commit-together-with-co-authors/
|
| 894 |
+
|
| 895 |
+
:return:
|
| 896 |
+
List of co-authors for this commit (as :class:`~git.util.Actor` objects).
|
| 897 |
+
"""
|
| 898 |
+
co_authors = []
|
| 899 |
+
|
| 900 |
+
if self.message:
|
| 901 |
+
results = re.findall(
|
| 902 |
+
r"^Co-authored-by: (.*) <(.*?)>$",
|
| 903 |
+
self.message,
|
| 904 |
+
re.MULTILINE,
|
| 905 |
+
)
|
| 906 |
+
for author in results:
|
| 907 |
+
co_authors.append(Actor(*author))
|
| 908 |
+
|
| 909 |
+
return co_authors
|
venv/lib/python3.10/site-packages/git/objects/fun.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
"""Functions that are supposed to be as fast as possible."""
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"tree_to_stream",
|
| 8 |
+
"tree_entries_from_data",
|
| 9 |
+
"traverse_trees_recursive",
|
| 10 |
+
"traverse_tree_recursive",
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
from stat import S_ISDIR
|
| 14 |
+
|
| 15 |
+
from git.compat import safe_decode, defenc
|
| 16 |
+
|
| 17 |
+
# typing ----------------------------------------------
|
| 18 |
+
|
| 19 |
+
from typing import (
|
| 20 |
+
Callable,
|
| 21 |
+
List,
|
| 22 |
+
MutableSequence,
|
| 23 |
+
Sequence,
|
| 24 |
+
Tuple,
|
| 25 |
+
TYPE_CHECKING,
|
| 26 |
+
Union,
|
| 27 |
+
overload,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
if TYPE_CHECKING:
|
| 31 |
+
from _typeshed import ReadableBuffer
|
| 32 |
+
|
| 33 |
+
from git import GitCmdObjectDB
|
| 34 |
+
|
| 35 |
+
EntryTup = Tuple[bytes, int, str] # Same as TreeCacheTup in tree.py.
|
| 36 |
+
EntryTupOrNone = Union[EntryTup, None]
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
|
| 42 |
+
"""Write the given list of entries into a stream using its ``write`` method.
|
| 43 |
+
|
| 44 |
+
:param entries:
|
| 45 |
+
**Sorted** list of tuples with (binsha, mode, name).
|
| 46 |
+
|
| 47 |
+
:param write:
|
| 48 |
+
A ``write`` method which takes a data string.
|
| 49 |
+
"""
|
| 50 |
+
ord_zero = ord("0")
|
| 51 |
+
bit_mask = 7 # 3 bits set.
|
| 52 |
+
|
| 53 |
+
for binsha, mode, name in entries:
|
| 54 |
+
mode_str = b""
|
| 55 |
+
for i in range(6):
|
| 56 |
+
mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str
|
| 57 |
+
# END for each 8 octal value
|
| 58 |
+
|
| 59 |
+
# git slices away the first octal if it's zero.
|
| 60 |
+
if mode_str[0] == ord_zero:
|
| 61 |
+
mode_str = mode_str[1:]
|
| 62 |
+
# END save a byte
|
| 63 |
+
|
| 64 |
+
# Here it comes: If the name is actually unicode, the replacement below will not
|
| 65 |
+
# work as the binsha is not part of the ascii unicode encoding - hence we must
|
| 66 |
+
# convert to an UTF-8 string for it to work properly. According to my tests,
|
| 67 |
+
# this is exactly what git does, that is it just takes the input literally,
|
| 68 |
+
# which appears to be UTF-8 on linux.
|
| 69 |
+
if isinstance(name, str):
|
| 70 |
+
name_bytes = name.encode(defenc)
|
| 71 |
+
else:
|
| 72 |
+
name_bytes = name # type: ignore[unreachable] # check runtime types - is always str?
|
| 73 |
+
write(b"".join((mode_str, b" ", name_bytes, b"\0", binsha)))
|
| 74 |
+
# END for each item
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def tree_entries_from_data(data: bytes) -> List[EntryTup]:
|
| 78 |
+
"""Read the binary representation of a tree and returns tuples of
|
| 79 |
+
:class:`~git.objects.tree.Tree` items.
|
| 80 |
+
|
| 81 |
+
:param data:
|
| 82 |
+
Data block with tree data (as bytes).
|
| 83 |
+
|
| 84 |
+
:return:
|
| 85 |
+
list(tuple(binsha, mode, tree_relative_path), ...)
|
| 86 |
+
"""
|
| 87 |
+
ord_zero = ord("0")
|
| 88 |
+
space_ord = ord(" ")
|
| 89 |
+
len_data = len(data)
|
| 90 |
+
i = 0
|
| 91 |
+
out = []
|
| 92 |
+
while i < len_data:
|
| 93 |
+
mode = 0
|
| 94 |
+
|
| 95 |
+
# Read Mode
|
| 96 |
+
# Some git versions truncate the leading 0, some don't.
|
| 97 |
+
# The type will be extracted from the mode later.
|
| 98 |
+
while data[i] != space_ord:
|
| 99 |
+
# Move existing mode integer up one level being 3 bits and add the actual
|
| 100 |
+
# ordinal value of the character.
|
| 101 |
+
mode = (mode << 3) + (data[i] - ord_zero)
|
| 102 |
+
i += 1
|
| 103 |
+
# END while reading mode
|
| 104 |
+
|
| 105 |
+
# Byte is space now, skip it.
|
| 106 |
+
i += 1
|
| 107 |
+
|
| 108 |
+
# Parse name, it is NULL separated.
|
| 109 |
+
|
| 110 |
+
ns = i
|
| 111 |
+
while data[i] != 0:
|
| 112 |
+
i += 1
|
| 113 |
+
# END while not reached NULL
|
| 114 |
+
|
| 115 |
+
# Default encoding for strings in git is UTF-8.
|
| 116 |
+
# Only use the respective unicode object if the byte stream was encoded.
|
| 117 |
+
name_bytes = data[ns:i]
|
| 118 |
+
name = safe_decode(name_bytes)
|
| 119 |
+
|
| 120 |
+
# Byte is NULL, get next 20.
|
| 121 |
+
i += 1
|
| 122 |
+
sha = data[i : i + 20]
|
| 123 |
+
i = i + 20
|
| 124 |
+
out.append((sha, mode, name))
|
| 125 |
+
# END for each byte in data stream
|
| 126 |
+
return out
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone:
|
| 130 |
+
"""Return data entry matching the given name and tree mode or ``None``.
|
| 131 |
+
|
| 132 |
+
Before the item is returned, the respective data item is set None in the `tree_data`
|
| 133 |
+
list to mark it done.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
item = tree_data[start_at]
|
| 138 |
+
if item and item[2] == name and S_ISDIR(item[1]) == is_dir:
|
| 139 |
+
tree_data[start_at] = None
|
| 140 |
+
return item
|
| 141 |
+
except IndexError:
|
| 142 |
+
pass
|
| 143 |
+
# END exception handling
|
| 144 |
+
for index, item in enumerate(tree_data):
|
| 145 |
+
if item and item[2] == name and S_ISDIR(item[1]) == is_dir:
|
| 146 |
+
tree_data[index] = None
|
| 147 |
+
return item
|
| 148 |
+
# END if item matches
|
| 149 |
+
# END for each item
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@overload
|
| 154 |
+
def _to_full_path(item: None, path_prefix: str) -> None: ...
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@overload
|
| 158 |
+
def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: ...
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone:
|
| 162 |
+
"""Rebuild entry with given path prefix."""
|
| 163 |
+
if not item:
|
| 164 |
+
return item
|
| 165 |
+
return (item[0], item[1], path_prefix + item[2])
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def traverse_trees_recursive(
|
| 169 |
+
odb: "GitCmdObjectDB", tree_shas: Sequence[Union[bytes, None]], path_prefix: str
|
| 170 |
+
) -> List[Tuple[EntryTupOrNone, ...]]:
|
| 171 |
+
"""
|
| 172 |
+
:return:
|
| 173 |
+
List of list with entries according to the given binary tree-shas.
|
| 174 |
+
|
| 175 |
+
The result is encoded in a list
|
| 176 |
+
of n tuple|None per blob/commit, (n == len(tree_shas)), where:
|
| 177 |
+
|
| 178 |
+
* [0] == 20 byte sha
|
| 179 |
+
* [1] == mode as int
|
| 180 |
+
* [2] == path relative to working tree root
|
| 181 |
+
|
| 182 |
+
The entry tuple is ``None`` if the respective blob/commit did not exist in the
|
| 183 |
+
given tree.
|
| 184 |
+
|
| 185 |
+
:param tree_shas:
|
| 186 |
+
Iterable of shas pointing to trees. All trees must be on the same level.
|
| 187 |
+
A tree-sha may be ``None``, in which case ``None``.
|
| 188 |
+
|
| 189 |
+
:param path_prefix:
|
| 190 |
+
A prefix to be added to the returned paths on this level.
|
| 191 |
+
Set it ``""`` for the first iteration.
|
| 192 |
+
|
| 193 |
+
:note:
|
| 194 |
+
The ordering of the returned items will be partially lost.
|
| 195 |
+
"""
|
| 196 |
+
trees_data: List[List[EntryTupOrNone]] = []
|
| 197 |
+
|
| 198 |
+
nt = len(tree_shas)
|
| 199 |
+
for tree_sha in tree_shas:
|
| 200 |
+
if tree_sha is None:
|
| 201 |
+
data: List[EntryTupOrNone] = []
|
| 202 |
+
else:
|
| 203 |
+
# Make new list for typing as list invariant.
|
| 204 |
+
data = list(tree_entries_from_data(odb.stream(tree_sha).read()))
|
| 205 |
+
# END handle muted trees
|
| 206 |
+
trees_data.append(data)
|
| 207 |
+
# END for each sha to get data for
|
| 208 |
+
|
| 209 |
+
out: List[Tuple[EntryTupOrNone, ...]] = []
|
| 210 |
+
|
| 211 |
+
# Find all matching entries and recursively process them together if the match is a
|
| 212 |
+
# tree. If the match is a non-tree item, put it into the result.
|
| 213 |
+
# Processed items will be set None.
|
| 214 |
+
for ti, tree_data in enumerate(trees_data):
|
| 215 |
+
for ii, item in enumerate(tree_data):
|
| 216 |
+
if not item:
|
| 217 |
+
continue
|
| 218 |
+
# END skip already done items
|
| 219 |
+
entries: List[EntryTupOrNone]
|
| 220 |
+
entries = [None for _ in range(nt)]
|
| 221 |
+
entries[ti] = item
|
| 222 |
+
_sha, mode, name = item
|
| 223 |
+
is_dir = S_ISDIR(mode) # Type mode bits
|
| 224 |
+
|
| 225 |
+
# Find this item in all other tree data items.
|
| 226 |
+
# Wrap around, but stop one before our current index, hence ti+nt, not
|
| 227 |
+
# ti+1+nt.
|
| 228 |
+
for tio in range(ti + 1, ti + nt):
|
| 229 |
+
tio = tio % nt
|
| 230 |
+
entries[tio] = _find_by_name(trees_data[tio], name, is_dir, ii)
|
| 231 |
+
|
| 232 |
+
# END for each other item data
|
| 233 |
+
# If we are a directory, enter recursion.
|
| 234 |
+
if is_dir:
|
| 235 |
+
out.extend(
|
| 236 |
+
traverse_trees_recursive(
|
| 237 |
+
odb,
|
| 238 |
+
[((ei and ei[0]) or None) for ei in entries],
|
| 239 |
+
path_prefix + name + "/",
|
| 240 |
+
)
|
| 241 |
+
)
|
| 242 |
+
else:
|
| 243 |
+
out.append(tuple(_to_full_path(e, path_prefix) for e in entries))
|
| 244 |
+
|
| 245 |
+
# END handle recursion
|
| 246 |
+
# Finally mark it done.
|
| 247 |
+
tree_data[ii] = None
|
| 248 |
+
# END for each item
|
| 249 |
+
|
| 250 |
+
# We are done with one tree, set all its data empty.
|
| 251 |
+
del tree_data[:]
|
| 252 |
+
# END for each tree_data chunk
|
| 253 |
+
return out
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]:
|
| 257 |
+
"""
|
| 258 |
+
:return:
|
| 259 |
+
List of entries of the tree pointed to by the binary `tree_sha`.
|
| 260 |
+
|
| 261 |
+
An entry has the following format:
|
| 262 |
+
|
| 263 |
+
* [0] 20 byte sha
|
| 264 |
+
* [1] mode as int
|
| 265 |
+
* [2] path relative to the repository
|
| 266 |
+
|
| 267 |
+
:param path_prefix:
|
| 268 |
+
Prefix to prepend to the front of all returned paths.
|
| 269 |
+
"""
|
| 270 |
+
entries = []
|
| 271 |
+
data = tree_entries_from_data(odb.stream(tree_sha).read())
|
| 272 |
+
|
| 273 |
+
# Unpacking/packing is faster than accessing individual items.
|
| 274 |
+
for sha, mode, name in data:
|
| 275 |
+
if S_ISDIR(mode):
|
| 276 |
+
entries.extend(traverse_tree_recursive(odb, sha, path_prefix + name + "/"))
|
| 277 |
+
else:
|
| 278 |
+
entries.append((sha, mode, path_prefix + name))
|
| 279 |
+
# END for each item
|
| 280 |
+
|
| 281 |
+
return entries
|
venv/lib/python3.10/site-packages/git/objects/submodule/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
__all__ = ["Submodule", "UpdateProgress", "RootModule", "RootUpdateProgress"]
|
| 5 |
+
|
| 6 |
+
from .base import Submodule, UpdateProgress
|
| 7 |
+
from .root import RootModule, RootUpdateProgress
|
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (351 Bytes). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/base.cpython-310.pyc
ADDED
|
Binary file (40.4 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/root.cpython-310.pyc
ADDED
|
Binary file (9.29 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/submodule/__pycache__/util.cpython-310.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
venv/lib/python3.10/site-packages/git/objects/submodule/base.py
ADDED
|
@@ -0,0 +1,1632 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
__all__ = ["Submodule", "UpdateProgress"]
|
| 5 |
+
|
| 6 |
+
import gc
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import os.path as osp
|
| 11 |
+
import stat
|
| 12 |
+
import sys
|
| 13 |
+
import uuid
|
| 14 |
+
|
| 15 |
+
import git
|
| 16 |
+
from git.cmd import Git
|
| 17 |
+
from git.compat import defenc
|
| 18 |
+
from git.config import GitConfigParser, SectionConstraint, cp
|
| 19 |
+
from git.exc import (
|
| 20 |
+
BadName,
|
| 21 |
+
InvalidGitRepositoryError,
|
| 22 |
+
NoSuchPathError,
|
| 23 |
+
RepositoryDirtyError,
|
| 24 |
+
)
|
| 25 |
+
from git.objects.base import IndexObject, Object
|
| 26 |
+
from git.objects.util import TraversableIterableObj
|
| 27 |
+
from git.util import (
|
| 28 |
+
IterableList,
|
| 29 |
+
RemoteProgress,
|
| 30 |
+
join_path_native,
|
| 31 |
+
rmtree,
|
| 32 |
+
to_native_path_linux,
|
| 33 |
+
unbare_repo,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
from .util import (
|
| 37 |
+
SubmoduleConfigParser,
|
| 38 |
+
find_first_remote_branch,
|
| 39 |
+
mkhead,
|
| 40 |
+
sm_name,
|
| 41 |
+
sm_section,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# typing ----------------------------------------------------------------------
|
| 45 |
+
|
| 46 |
+
from typing import (
|
| 47 |
+
Any,
|
| 48 |
+
Callable,
|
| 49 |
+
Dict,
|
| 50 |
+
Iterator,
|
| 51 |
+
Mapping,
|
| 52 |
+
Sequence,
|
| 53 |
+
TYPE_CHECKING,
|
| 54 |
+
Union,
|
| 55 |
+
cast,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
if sys.version_info >= (3, 8):
|
| 59 |
+
from typing import Literal
|
| 60 |
+
else:
|
| 61 |
+
from typing_extensions import Literal
|
| 62 |
+
|
| 63 |
+
from git.types import Commit_ish, PathLike, TBD
|
| 64 |
+
|
| 65 |
+
if TYPE_CHECKING:
|
| 66 |
+
from git.index import IndexFile
|
| 67 |
+
from git.objects.commit import Commit
|
| 68 |
+
from git.refs import Head
|
| 69 |
+
from git.repo import Repo
|
| 70 |
+
|
| 71 |
+
# -----------------------------------------------------------------------------
|
| 72 |
+
|
| 73 |
+
_logger = logging.getLogger(__name__)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class UpdateProgress(RemoteProgress):
|
| 77 |
+
"""Class providing detailed progress information to the caller who should
|
| 78 |
+
derive from it and implement the
|
| 79 |
+
:meth:`update(...) <git.util.RemoteProgress.update>` message."""
|
| 80 |
+
|
| 81 |
+
CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
|
| 82 |
+
_num_op_codes: int = RemoteProgress._num_op_codes + 3
|
| 83 |
+
|
| 84 |
+
__slots__ = ()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
BEGIN = UpdateProgress.BEGIN
|
| 88 |
+
END = UpdateProgress.END
|
| 89 |
+
CLONE = UpdateProgress.CLONE
|
| 90 |
+
FETCH = UpdateProgress.FETCH
|
| 91 |
+
UPDWKTREE = UpdateProgress.UPDWKTREE
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
|
| 95 |
+
# mechanism, which causes plenty of trouble if the only reason for packages and modules
|
| 96 |
+
# is refactoring - subpackages shouldn't depend on parent packages.
|
| 97 |
+
class Submodule(IndexObject, TraversableIterableObj):
|
| 98 |
+
"""Implements access to a git submodule. They are special in that their sha
|
| 99 |
+
represents a commit in the submodule's repository which is to be checked out
|
| 100 |
+
at the path of this instance.
|
| 101 |
+
|
| 102 |
+
The submodule type does not have a string type associated with it, as it exists
|
| 103 |
+
solely as a marker in the tree and index.
|
| 104 |
+
|
| 105 |
+
All methods work in bare and non-bare repositories.
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
_id_attribute_ = "name"
|
| 109 |
+
k_modules_file = ".gitmodules"
|
| 110 |
+
k_head_option = "branch"
|
| 111 |
+
k_head_default = "master"
|
| 112 |
+
k_default_mode = stat.S_IFDIR | stat.S_IFLNK
|
| 113 |
+
"""Submodule flags. Submodules are directories with link-status."""
|
| 114 |
+
|
| 115 |
+
type: Literal["submodule"] = "submodule" # type: ignore[assignment]
|
| 116 |
+
"""This is a bogus type string for base class compatibility."""
|
| 117 |
+
|
| 118 |
+
__slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
|
| 119 |
+
|
| 120 |
+
_cache_attrs = ("path", "_url", "_branch_path")
|
| 121 |
+
|
| 122 |
+
def __init__(
|
| 123 |
+
self,
|
| 124 |
+
repo: "Repo",
|
| 125 |
+
binsha: bytes,
|
| 126 |
+
mode: Union[int, None] = None,
|
| 127 |
+
path: Union[PathLike, None] = None,
|
| 128 |
+
name: Union[str, None] = None,
|
| 129 |
+
parent_commit: Union["Commit", None] = None,
|
| 130 |
+
url: Union[str, None] = None,
|
| 131 |
+
branch_path: Union[PathLike, None] = None,
|
| 132 |
+
) -> None:
|
| 133 |
+
"""Initialize this instance with its attributes.
|
| 134 |
+
|
| 135 |
+
We only document the parameters that differ from
|
| 136 |
+
:class:`~git.objects.base.IndexObject`.
|
| 137 |
+
|
| 138 |
+
:param repo:
|
| 139 |
+
Our parent repository.
|
| 140 |
+
|
| 141 |
+
:param binsha:
|
| 142 |
+
Binary sha referring to a commit in the remote repository.
|
| 143 |
+
See the `url` parameter.
|
| 144 |
+
|
| 145 |
+
:param parent_commit:
|
| 146 |
+
The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
|
| 147 |
+
the ``.gitmodules`` blob, or ``None`` to always point to the most recent
|
| 148 |
+
commit. See :meth:`set_parent_commit` for details.
|
| 149 |
+
|
| 150 |
+
:param url:
|
| 151 |
+
The URL to the remote repository which is the submodule.
|
| 152 |
+
|
| 153 |
+
:param branch_path:
|
| 154 |
+
Full repository-relative path to ref to checkout when cloning the remote
|
| 155 |
+
repository.
|
| 156 |
+
"""
|
| 157 |
+
super().__init__(repo, binsha, mode, path)
|
| 158 |
+
self.size = 0
|
| 159 |
+
self._parent_commit = parent_commit
|
| 160 |
+
if url is not None:
|
| 161 |
+
self._url = url
|
| 162 |
+
if branch_path is not None:
|
| 163 |
+
self._branch_path = branch_path
|
| 164 |
+
if name is not None:
|
| 165 |
+
self._name = name
|
| 166 |
+
|
| 167 |
+
def _set_cache_(self, attr: str) -> None:
|
| 168 |
+
if attr in ("path", "_url", "_branch_path"):
|
| 169 |
+
reader: SectionConstraint = self.config_reader()
|
| 170 |
+
# Default submodule values.
|
| 171 |
+
try:
|
| 172 |
+
self.path = reader.get("path")
|
| 173 |
+
except cp.NoSectionError as e:
|
| 174 |
+
if self.repo.working_tree_dir is not None:
|
| 175 |
+
raise ValueError(
|
| 176 |
+
"This submodule instance does not exist anymore in '%s' file"
|
| 177 |
+
% osp.join(self.repo.working_tree_dir, ".gitmodules")
|
| 178 |
+
) from e
|
| 179 |
+
|
| 180 |
+
self._url = reader.get("url")
|
| 181 |
+
# GitPython extension values - optional.
|
| 182 |
+
self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
|
| 183 |
+
elif attr == "_name":
|
| 184 |
+
raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
|
| 185 |
+
else:
|
| 186 |
+
super()._set_cache_(attr)
|
| 187 |
+
# END handle attribute name
|
| 188 |
+
|
| 189 |
+
@classmethod
|
| 190 |
+
def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]:
|
| 191 |
+
""":return: All the submodules of our module repository"""
|
| 192 |
+
try:
|
| 193 |
+
return cls.list_items(item.module())
|
| 194 |
+
except InvalidGitRepositoryError:
|
| 195 |
+
return IterableList("")
|
| 196 |
+
# END handle intermediate items
|
| 197 |
+
|
| 198 |
+
@classmethod
|
| 199 |
+
def _need_gitfile_submodules(cls, git: Git) -> bool:
|
| 200 |
+
return git.version_info[:3] >= (1, 7, 5)
|
| 201 |
+
|
| 202 |
+
def __eq__(self, other: Any) -> bool:
|
| 203 |
+
"""Compare with another submodule."""
|
| 204 |
+
# We may only compare by name as this should be the ID they are hashed with.
|
| 205 |
+
# Otherwise this type wouldn't be hashable.
|
| 206 |
+
# return self.path == other.path and self.url == other.url and super().__eq__(other)
|
| 207 |
+
return self._name == other._name
|
| 208 |
+
|
| 209 |
+
def __ne__(self, other: object) -> bool:
|
| 210 |
+
"""Compare with another submodule for inequality."""
|
| 211 |
+
return not (self == other)
|
| 212 |
+
|
| 213 |
+
def __hash__(self) -> int:
|
| 214 |
+
"""Hash this instance using its logical id, not the sha."""
|
| 215 |
+
return hash(self._name)
|
| 216 |
+
|
| 217 |
+
def __str__(self) -> str:
|
| 218 |
+
return self._name
|
| 219 |
+
|
| 220 |
+
def __repr__(self) -> str:
|
| 221 |
+
return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % (
|
| 222 |
+
type(self).__name__,
|
| 223 |
+
self._name,
|
| 224 |
+
self.path,
|
| 225 |
+
self.url,
|
| 226 |
+
self.branch_path,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
@classmethod
|
| 230 |
+
def _config_parser(
|
| 231 |
+
cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
|
| 232 |
+
) -> SubmoduleConfigParser:
|
| 233 |
+
"""
|
| 234 |
+
:return:
|
| 235 |
+
Config parser constrained to our submodule in read or write mode
|
| 236 |
+
|
| 237 |
+
:raise IOError:
|
| 238 |
+
If the ``.gitmodules`` file cannot be found, either locally or in the
|
| 239 |
+
repository at the given parent commit. Otherwise the exception would be
|
| 240 |
+
delayed until the first access of the config parser.
|
| 241 |
+
"""
|
| 242 |
+
parent_matches_head = True
|
| 243 |
+
if parent_commit is not None:
|
| 244 |
+
try:
|
| 245 |
+
parent_matches_head = repo.head.commit == parent_commit
|
| 246 |
+
except ValueError:
|
| 247 |
+
# We are most likely in an empty repository, so the HEAD doesn't point
|
| 248 |
+
# to a valid ref.
|
| 249 |
+
pass
|
| 250 |
+
# END handle parent_commit
|
| 251 |
+
fp_module: Union[str, BytesIO]
|
| 252 |
+
if not repo.bare and parent_matches_head and repo.working_tree_dir:
|
| 253 |
+
fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
|
| 254 |
+
else:
|
| 255 |
+
assert parent_commit is not None, "need valid parent_commit in bare repositories"
|
| 256 |
+
try:
|
| 257 |
+
fp_module = cls._sio_modules(parent_commit)
|
| 258 |
+
except KeyError as e:
|
| 259 |
+
raise IOError(
|
| 260 |
+
"Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit)
|
| 261 |
+
) from e
|
| 262 |
+
# END handle exceptions
|
| 263 |
+
# END handle non-bare working tree
|
| 264 |
+
|
| 265 |
+
if not read_only and (repo.bare or not parent_matches_head):
|
| 266 |
+
raise ValueError("Cannot write blobs of 'historical' submodule configurations")
|
| 267 |
+
# END handle writes of historical submodules
|
| 268 |
+
|
| 269 |
+
return SubmoduleConfigParser(fp_module, read_only=read_only)
|
| 270 |
+
|
| 271 |
+
def _clear_cache(self) -> None:
|
| 272 |
+
"""Clear the possibly changed values."""
|
| 273 |
+
for name in self._cache_attrs:
|
| 274 |
+
try:
|
| 275 |
+
delattr(self, name)
|
| 276 |
+
except AttributeError:
|
| 277 |
+
pass
|
| 278 |
+
# END try attr deletion
|
| 279 |
+
# END for each name to delete
|
| 280 |
+
|
| 281 |
+
@classmethod
|
| 282 |
+
def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
|
| 283 |
+
"""
|
| 284 |
+
:return:
|
| 285 |
+
Configuration file as :class:`~io.BytesIO` - we only access it through the
|
| 286 |
+
respective blob's data
|
| 287 |
+
"""
|
| 288 |
+
sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
|
| 289 |
+
sio.name = cls.k_modules_file
|
| 290 |
+
return sio
|
| 291 |
+
|
| 292 |
+
def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
|
| 293 |
+
""":return: Config parser constrained to our submodule in read or write mode"""
|
| 294 |
+
try:
|
| 295 |
+
pc = self.parent_commit
|
| 296 |
+
except ValueError:
|
| 297 |
+
pc = None
|
| 298 |
+
# END handle empty parent repository
|
| 299 |
+
parser = self._config_parser(self.repo, pc, read_only)
|
| 300 |
+
parser.set_submodule(self)
|
| 301 |
+
return SectionConstraint(parser, sm_section(self.name))
|
| 302 |
+
|
| 303 |
+
@classmethod
|
| 304 |
+
def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
|
| 305 |
+
if cls._need_gitfile_submodules(parent_repo.git):
|
| 306 |
+
return osp.join(parent_repo.git_dir, "modules", name)
|
| 307 |
+
if parent_repo.working_tree_dir:
|
| 308 |
+
return osp.join(parent_repo.working_tree_dir, path)
|
| 309 |
+
raise NotADirectoryError()
|
| 310 |
+
|
| 311 |
+
@classmethod
|
| 312 |
+
def _clone_repo(
|
| 313 |
+
cls,
|
| 314 |
+
repo: "Repo",
|
| 315 |
+
url: str,
|
| 316 |
+
path: PathLike,
|
| 317 |
+
name: str,
|
| 318 |
+
allow_unsafe_options: bool = False,
|
| 319 |
+
allow_unsafe_protocols: bool = False,
|
| 320 |
+
**kwargs: Any,
|
| 321 |
+
) -> "Repo":
|
| 322 |
+
"""
|
| 323 |
+
:return:
|
| 324 |
+
:class:`~git.repo.base.Repo` instance of newly cloned repository.
|
| 325 |
+
|
| 326 |
+
:param repo:
|
| 327 |
+
Our parent repository.
|
| 328 |
+
|
| 329 |
+
:param url:
|
| 330 |
+
URL to clone from.
|
| 331 |
+
|
| 332 |
+
:param path:
|
| 333 |
+
Repository-relative path to the submodule checkout location.
|
| 334 |
+
|
| 335 |
+
:param name:
|
| 336 |
+
Canonical name of the submodule.
|
| 337 |
+
|
| 338 |
+
:param allow_unsafe_protocols:
|
| 339 |
+
Allow unsafe protocols to be used, like ``ext``.
|
| 340 |
+
|
| 341 |
+
:param allow_unsafe_options:
|
| 342 |
+
Allow unsafe options to be used, like ``--upload-pack``.
|
| 343 |
+
|
| 344 |
+
:param kwargs:
|
| 345 |
+
Additional arguments given to :manpage:`git-clone(1)`.
|
| 346 |
+
"""
|
| 347 |
+
module_abspath = cls._module_abspath(repo, path, name)
|
| 348 |
+
module_checkout_path = module_abspath
|
| 349 |
+
if cls._need_gitfile_submodules(repo.git):
|
| 350 |
+
kwargs["separate_git_dir"] = module_abspath
|
| 351 |
+
module_abspath_dir = osp.dirname(module_abspath)
|
| 352 |
+
if not osp.isdir(module_abspath_dir):
|
| 353 |
+
os.makedirs(module_abspath_dir)
|
| 354 |
+
module_checkout_path = osp.join(str(repo.working_tree_dir), path)
|
| 355 |
+
|
| 356 |
+
clone = git.Repo.clone_from(
|
| 357 |
+
url,
|
| 358 |
+
module_checkout_path,
|
| 359 |
+
allow_unsafe_options=allow_unsafe_options,
|
| 360 |
+
allow_unsafe_protocols=allow_unsafe_protocols,
|
| 361 |
+
**kwargs,
|
| 362 |
+
)
|
| 363 |
+
if cls._need_gitfile_submodules(repo.git):
|
| 364 |
+
cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
|
| 365 |
+
|
| 366 |
+
return clone
|
| 367 |
+
|
| 368 |
+
@classmethod
|
| 369 |
+
def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
|
| 370 |
+
""":return: A path guaranteed to be relative to the given parent repository
|
| 371 |
+
|
| 372 |
+
:raise ValueError:
|
| 373 |
+
If path is not contained in the parent repository's working tree.
|
| 374 |
+
"""
|
| 375 |
+
path = to_native_path_linux(path)
|
| 376 |
+
if path.endswith("/"):
|
| 377 |
+
path = path[:-1]
|
| 378 |
+
# END handle trailing slash
|
| 379 |
+
|
| 380 |
+
if osp.isabs(path) and parent_repo.working_tree_dir:
|
| 381 |
+
working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
|
| 382 |
+
if not path.startswith(working_tree_linux):
|
| 383 |
+
raise ValueError(
|
| 384 |
+
"Submodule checkout path '%s' needs to be within the parents repository at '%s'"
|
| 385 |
+
% (working_tree_linux, path)
|
| 386 |
+
)
|
| 387 |
+
path = path[len(working_tree_linux.rstrip("/")) + 1 :]
|
| 388 |
+
if not path:
|
| 389 |
+
raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
|
| 390 |
+
# END verify converted relative path makes sense
|
| 391 |
+
# END convert to a relative path
|
| 392 |
+
|
| 393 |
+
return path
|
| 394 |
+
|
| 395 |
+
@classmethod
|
| 396 |
+
def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
|
| 397 |
+
"""Write a ``.git`` file containing a (preferably) relative path to the actual
|
| 398 |
+
git module repository.
|
| 399 |
+
|
| 400 |
+
It is an error if the `module_abspath` cannot be made into a relative path,
|
| 401 |
+
relative to the `working_tree_dir`.
|
| 402 |
+
|
| 403 |
+
:note:
|
| 404 |
+
This will overwrite existing files!
|
| 405 |
+
|
| 406 |
+
:note:
|
| 407 |
+
As we rewrite both the git file as well as the module configuration, we
|
| 408 |
+
might fail on the configuration and will not roll back changes done to the
|
| 409 |
+
git file. This should be a non-issue, but may easily be fixed if it becomes
|
| 410 |
+
one.
|
| 411 |
+
|
| 412 |
+
:param working_tree_dir:
|
| 413 |
+
Directory to write the ``.git`` file into.
|
| 414 |
+
|
| 415 |
+
:param module_abspath:
|
| 416 |
+
Absolute path to the bare repository.
|
| 417 |
+
"""
|
| 418 |
+
git_file = osp.join(working_tree_dir, ".git")
|
| 419 |
+
rela_path = osp.relpath(module_abspath, start=working_tree_dir)
|
| 420 |
+
if sys.platform == "win32" and osp.isfile(git_file):
|
| 421 |
+
os.remove(git_file)
|
| 422 |
+
with open(git_file, "wb") as fp:
|
| 423 |
+
fp.write(("gitdir: %s" % rela_path).encode(defenc))
|
| 424 |
+
|
| 425 |
+
with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer:
|
| 426 |
+
writer.set_value(
|
| 427 |
+
"core",
|
| 428 |
+
"worktree",
|
| 429 |
+
to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)),
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# { Edit Interface
|
| 433 |
+
|
| 434 |
+
@classmethod
|
| 435 |
+
def add(
|
| 436 |
+
cls,
|
| 437 |
+
repo: "Repo",
|
| 438 |
+
name: str,
|
| 439 |
+
path: PathLike,
|
| 440 |
+
url: Union[str, None] = None,
|
| 441 |
+
branch: Union[str, None] = None,
|
| 442 |
+
no_checkout: bool = False,
|
| 443 |
+
depth: Union[int, None] = None,
|
| 444 |
+
env: Union[Mapping[str, str], None] = None,
|
| 445 |
+
clone_multi_options: Union[Sequence[TBD], None] = None,
|
| 446 |
+
allow_unsafe_options: bool = False,
|
| 447 |
+
allow_unsafe_protocols: bool = False,
|
| 448 |
+
) -> "Submodule":
|
| 449 |
+
"""Add a new submodule to the given repository. This will alter the index as
|
| 450 |
+
well as the ``.gitmodules`` file, but will not create a new commit. If the
|
| 451 |
+
submodule already exists, no matter if the configuration differs from the one
|
| 452 |
+
provided, the existing submodule will be returned.
|
| 453 |
+
|
| 454 |
+
:param repo:
|
| 455 |
+
Repository instance which should receive the submodule.
|
| 456 |
+
|
| 457 |
+
:param name:
|
| 458 |
+
The name/identifier for the submodule.
|
| 459 |
+
|
| 460 |
+
:param path:
|
| 461 |
+
Repository-relative or absolute path at which the submodule should be
|
| 462 |
+
located.
|
| 463 |
+
It will be created as required during the repository initialization.
|
| 464 |
+
|
| 465 |
+
:param url:
|
| 466 |
+
``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more
|
| 467 |
+
information. If ``None``, the repository is assumed to exist, and the URL of
|
| 468 |
+
the first remote is taken instead. This is useful if you want to make an
|
| 469 |
+
existing repository a submodule of another one.
|
| 470 |
+
|
| 471 |
+
:param branch:
|
| 472 |
+
Name of branch at which the submodule should (later) be checked out. The
|
| 473 |
+
given branch must exist in the remote repository, and will be checked out
|
| 474 |
+
locally as a tracking branch.
|
| 475 |
+
It will only be written into the configuration if it not ``None``, which is
|
| 476 |
+
when the checked out branch will be the one the remote HEAD pointed to.
|
| 477 |
+
The result you get in these situation is somewhat fuzzy, and it is
|
| 478 |
+
recommended to specify at least ``master`` here.
|
| 479 |
+
Examples are ``master`` or ``feature/new``.
|
| 480 |
+
|
| 481 |
+
:param no_checkout:
|
| 482 |
+
If ``True``, and if the repository has to be cloned manually, no checkout
|
| 483 |
+
will be performed.
|
| 484 |
+
|
| 485 |
+
:param depth:
|
| 486 |
+
Create a shallow clone with a history truncated to the specified number of
|
| 487 |
+
commits.
|
| 488 |
+
|
| 489 |
+
:param env:
|
| 490 |
+
Optional dictionary containing the desired environment variables.
|
| 491 |
+
|
| 492 |
+
Note: Provided variables will be used to update the execution environment
|
| 493 |
+
for ``git``. If some variable is not specified in `env` and is defined in
|
| 494 |
+
attr:`os.environ`, the value from attr:`os.environ` will be used. If you
|
| 495 |
+
want to unset some variable, consider providing an empty string as its
|
| 496 |
+
value.
|
| 497 |
+
|
| 498 |
+
:param clone_multi_options:
|
| 499 |
+
A list of clone options. Please see
|
| 500 |
+
:meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
|
| 501 |
+
|
| 502 |
+
:param allow_unsafe_protocols:
|
| 503 |
+
Allow unsafe protocols to be used, like ``ext``.
|
| 504 |
+
|
| 505 |
+
:param allow_unsafe_options:
|
| 506 |
+
Allow unsafe options to be used, like ``--upload-pack``.
|
| 507 |
+
|
| 508 |
+
:return:
|
| 509 |
+
The newly created :class:`Submodule` instance.
|
| 510 |
+
|
| 511 |
+
:note:
|
| 512 |
+
Works atomically, such that no change will be done if, for example, the
|
| 513 |
+
repository update fails.
|
| 514 |
+
"""
|
| 515 |
+
if repo.bare:
|
| 516 |
+
raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
|
| 517 |
+
# END handle bare repos
|
| 518 |
+
|
| 519 |
+
path = cls._to_relative_path(repo, path)
|
| 520 |
+
|
| 521 |
+
# Ensure we never put backslashes into the URL, as might happen on Windows.
|
| 522 |
+
if url is not None:
|
| 523 |
+
url = to_native_path_linux(url)
|
| 524 |
+
# END ensure URL correctness
|
| 525 |
+
|
| 526 |
+
# INSTANTIATE INTERMEDIATE SM
|
| 527 |
+
sm = cls(
|
| 528 |
+
repo,
|
| 529 |
+
cls.NULL_BIN_SHA,
|
| 530 |
+
cls.k_default_mode,
|
| 531 |
+
path,
|
| 532 |
+
name,
|
| 533 |
+
url="invalid-temporary",
|
| 534 |
+
)
|
| 535 |
+
if sm.exists():
|
| 536 |
+
# Reretrieve submodule from tree.
|
| 537 |
+
try:
|
| 538 |
+
sm = repo.head.commit.tree[str(path)]
|
| 539 |
+
sm._name = name
|
| 540 |
+
return sm
|
| 541 |
+
except KeyError:
|
| 542 |
+
# Could only be in index.
|
| 543 |
+
index = repo.index
|
| 544 |
+
entry = index.entries[index.entry_key(path, 0)]
|
| 545 |
+
sm.binsha = entry.binsha
|
| 546 |
+
return sm
|
| 547 |
+
# END handle exceptions
|
| 548 |
+
# END handle existing
|
| 549 |
+
|
| 550 |
+
# fake-repo - we only need the functionality on the branch instance.
|
| 551 |
+
br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
|
| 552 |
+
has_module = sm.module_exists()
|
| 553 |
+
branch_is_default = branch is None
|
| 554 |
+
if has_module and url is not None:
|
| 555 |
+
if url not in [r.url for r in sm.module().remotes]:
|
| 556 |
+
raise ValueError(
|
| 557 |
+
"Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath)
|
| 558 |
+
)
|
| 559 |
+
# END check url
|
| 560 |
+
# END verify urls match
|
| 561 |
+
|
| 562 |
+
mrepo: Union[Repo, None] = None
|
| 563 |
+
|
| 564 |
+
if url is None:
|
| 565 |
+
if not has_module:
|
| 566 |
+
raise ValueError("A URL was not given and a repository did not exist at %s" % path)
|
| 567 |
+
# END check url
|
| 568 |
+
mrepo = sm.module()
|
| 569 |
+
# assert isinstance(mrepo, git.Repo)
|
| 570 |
+
urls = [r.url for r in mrepo.remotes]
|
| 571 |
+
if not urls:
|
| 572 |
+
raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath)
|
| 573 |
+
# END verify we have url
|
| 574 |
+
url = urls[0]
|
| 575 |
+
else:
|
| 576 |
+
# Clone new repo.
|
| 577 |
+
kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout}
|
| 578 |
+
if not branch_is_default:
|
| 579 |
+
kwargs["b"] = br.name
|
| 580 |
+
# END setup checkout-branch
|
| 581 |
+
|
| 582 |
+
if depth:
|
| 583 |
+
if isinstance(depth, int):
|
| 584 |
+
kwargs["depth"] = depth
|
| 585 |
+
else:
|
| 586 |
+
raise ValueError("depth should be an integer")
|
| 587 |
+
if clone_multi_options:
|
| 588 |
+
kwargs["multi_options"] = clone_multi_options
|
| 589 |
+
|
| 590 |
+
# _clone_repo(cls, repo, url, path, name, **kwargs):
|
| 591 |
+
mrepo = cls._clone_repo(
|
| 592 |
+
repo,
|
| 593 |
+
url,
|
| 594 |
+
path,
|
| 595 |
+
name,
|
| 596 |
+
env=env,
|
| 597 |
+
allow_unsafe_options=allow_unsafe_options,
|
| 598 |
+
allow_unsafe_protocols=allow_unsafe_protocols,
|
| 599 |
+
**kwargs,
|
| 600 |
+
)
|
| 601 |
+
# END verify url
|
| 602 |
+
|
| 603 |
+
## See #525 for ensuring git URLs in config-files are valid under Windows.
|
| 604 |
+
url = Git.polish_url(url)
|
| 605 |
+
|
| 606 |
+
# It's important to add the URL to the parent config, to let `git submodule` know.
|
| 607 |
+
# Otherwise there is a '-' character in front of the submodule listing:
|
| 608 |
+
# a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
|
| 609 |
+
# -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
|
| 610 |
+
writer: Union[GitConfigParser, SectionConstraint]
|
| 611 |
+
|
| 612 |
+
with sm.repo.config_writer() as writer:
|
| 613 |
+
writer.set_value(sm_section(name), "url", url)
|
| 614 |
+
|
| 615 |
+
# Update configuration and index.
|
| 616 |
+
index = sm.repo.index
|
| 617 |
+
with sm.config_writer(index=index, write=False) as writer:
|
| 618 |
+
writer.set_value("url", url)
|
| 619 |
+
writer.set_value("path", path)
|
| 620 |
+
|
| 621 |
+
sm._url = url
|
| 622 |
+
if not branch_is_default:
|
| 623 |
+
# Store full path.
|
| 624 |
+
writer.set_value(cls.k_head_option, br.path)
|
| 625 |
+
sm._branch_path = br.path
|
| 626 |
+
|
| 627 |
+
# We deliberately assume that our head matches our index!
|
| 628 |
+
if mrepo:
|
| 629 |
+
sm.binsha = mrepo.head.commit.binsha
|
| 630 |
+
index.add([sm], write=True)
|
| 631 |
+
|
| 632 |
+
return sm
|
| 633 |
+
|
| 634 |
+
def update(
|
| 635 |
+
self,
|
| 636 |
+
recursive: bool = False,
|
| 637 |
+
init: bool = True,
|
| 638 |
+
to_latest_revision: bool = False,
|
| 639 |
+
progress: Union["UpdateProgress", None] = None,
|
| 640 |
+
dry_run: bool = False,
|
| 641 |
+
force: bool = False,
|
| 642 |
+
keep_going: bool = False,
|
| 643 |
+
env: Union[Mapping[str, str], None] = None,
|
| 644 |
+
clone_multi_options: Union[Sequence[TBD], None] = None,
|
| 645 |
+
allow_unsafe_options: bool = False,
|
| 646 |
+
allow_unsafe_protocols: bool = False,
|
| 647 |
+
) -> "Submodule":
|
| 648 |
+
"""Update the repository of this submodule to point to the checkout we point at
|
| 649 |
+
with the binsha of this instance.
|
| 650 |
+
|
| 651 |
+
:param recursive:
|
| 652 |
+
If ``True``, we will operate recursively and update child modules as well.
|
| 653 |
+
|
| 654 |
+
:param init:
|
| 655 |
+
If ``True``, the module repository will be cloned into place if necessary.
|
| 656 |
+
|
| 657 |
+
:param to_latest_revision:
|
| 658 |
+
If ``True``, the submodule's sha will be ignored during checkout. Instead,
|
| 659 |
+
the remote will be fetched, and the local tracking branch updated. This only
|
| 660 |
+
works if we have a local tracking branch, which is the case if the remote
|
| 661 |
+
repository had a master branch, or if the ``branch`` option was specified
|
| 662 |
+
for this submodule and the branch existed remotely.
|
| 663 |
+
|
| 664 |
+
:param progress:
|
| 665 |
+
:class:`UpdateProgress` instance, or ``None`` if no progress should be
|
| 666 |
+
shown.
|
| 667 |
+
|
| 668 |
+
:param dry_run:
|
| 669 |
+
If ``True``, the operation will only be simulated, but not performed.
|
| 670 |
+
All performed operations are read-only.
|
| 671 |
+
|
| 672 |
+
:param force:
|
| 673 |
+
If ``True``, we may reset heads even if the repository in question is dirty.
|
| 674 |
+
Additionally we will be allowed to set a tracking branch which is ahead of
|
| 675 |
+
its remote branch back into the past or the location of the remote branch.
|
| 676 |
+
This will essentially 'forget' commits.
|
| 677 |
+
|
| 678 |
+
If ``False``, local tracking branches that are in the future of their
|
| 679 |
+
respective remote branches will simply not be moved.
|
| 680 |
+
|
| 681 |
+
:param keep_going:
|
| 682 |
+
If ``True``, we will ignore but log all errors, and keep going recursively.
|
| 683 |
+
Unless `dry_run` is set as well, `keep_going` could cause
|
| 684 |
+
subsequent/inherited errors you wouldn't see otherwise.
|
| 685 |
+
In conjunction with `dry_run`, it can be useful to anticipate all errors
|
| 686 |
+
when updating submodules.
|
| 687 |
+
|
| 688 |
+
:param env:
|
| 689 |
+
Optional dictionary containing the desired environment variables.
|
| 690 |
+
|
| 691 |
+
Note: Provided variables will be used to update the execution environment
|
| 692 |
+
for ``git``. If some variable is not specified in `env` and is defined in
|
| 693 |
+
attr:`os.environ`, value from attr:`os.environ` will be used.
|
| 694 |
+
|
| 695 |
+
If you want to unset some variable, consider providing the empty string as
|
| 696 |
+
its value.
|
| 697 |
+
|
| 698 |
+
:param clone_multi_options:
|
| 699 |
+
List of :manpage:`git-clone(1)` options.
|
| 700 |
+
Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
|
| 701 |
+
They only take effect with the `init` option.
|
| 702 |
+
|
| 703 |
+
:param allow_unsafe_protocols:
|
| 704 |
+
Allow unsafe protocols to be used, like ``ext``.
|
| 705 |
+
|
| 706 |
+
:param allow_unsafe_options:
|
| 707 |
+
Allow unsafe options to be used, like ``--upload-pack``.
|
| 708 |
+
|
| 709 |
+
:note:
|
| 710 |
+
Does nothing in bare repositories.
|
| 711 |
+
|
| 712 |
+
:note:
|
| 713 |
+
This method is definitely not atomic if `recursive` is ``True``.
|
| 714 |
+
|
| 715 |
+
:return:
|
| 716 |
+
self
|
| 717 |
+
"""
|
| 718 |
+
if self.repo.bare:
|
| 719 |
+
return self
|
| 720 |
+
# END pass in bare mode
|
| 721 |
+
|
| 722 |
+
if progress is None:
|
| 723 |
+
progress = UpdateProgress()
|
| 724 |
+
# END handle progress
|
| 725 |
+
prefix = ""
|
| 726 |
+
if dry_run:
|
| 727 |
+
prefix = "DRY-RUN: "
|
| 728 |
+
# END handle prefix
|
| 729 |
+
|
| 730 |
+
# To keep things plausible in dry-run mode.
|
| 731 |
+
if dry_run:
|
| 732 |
+
mrepo = None
|
| 733 |
+
# END init mrepo
|
| 734 |
+
|
| 735 |
+
try:
|
| 736 |
+
# ENSURE REPO IS PRESENT AND UP-TO-DATE
|
| 737 |
+
#######################################
|
| 738 |
+
try:
|
| 739 |
+
mrepo = self.module()
|
| 740 |
+
rmts = mrepo.remotes
|
| 741 |
+
len_rmts = len(rmts)
|
| 742 |
+
for i, remote in enumerate(rmts):
|
| 743 |
+
op = FETCH
|
| 744 |
+
if i == 0:
|
| 745 |
+
op |= BEGIN
|
| 746 |
+
# END handle start
|
| 747 |
+
|
| 748 |
+
progress.update(
|
| 749 |
+
op,
|
| 750 |
+
i,
|
| 751 |
+
len_rmts,
|
| 752 |
+
prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
|
| 753 |
+
)
|
| 754 |
+
# ===============================
|
| 755 |
+
if not dry_run:
|
| 756 |
+
remote.fetch(progress=progress)
|
| 757 |
+
# END handle dry-run
|
| 758 |
+
# ===============================
|
| 759 |
+
if i == len_rmts - 1:
|
| 760 |
+
op |= END
|
| 761 |
+
# END handle end
|
| 762 |
+
progress.update(
|
| 763 |
+
op,
|
| 764 |
+
i,
|
| 765 |
+
len_rmts,
|
| 766 |
+
prefix + "Done fetching remote of submodule %r" % self.name,
|
| 767 |
+
)
|
| 768 |
+
# END fetch new data
|
| 769 |
+
except InvalidGitRepositoryError:
|
| 770 |
+
mrepo = None
|
| 771 |
+
if not init:
|
| 772 |
+
return self
|
| 773 |
+
# END early abort if init is not allowed
|
| 774 |
+
|
| 775 |
+
# There is no git-repository yet - but delete empty paths.
|
| 776 |
+
checkout_module_abspath = self.abspath
|
| 777 |
+
if not dry_run and osp.isdir(checkout_module_abspath):
|
| 778 |
+
try:
|
| 779 |
+
os.rmdir(checkout_module_abspath)
|
| 780 |
+
except OSError as e:
|
| 781 |
+
raise OSError(
|
| 782 |
+
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
|
| 783 |
+
) from e
|
| 784 |
+
# END handle OSError
|
| 785 |
+
# END handle directory removal
|
| 786 |
+
|
| 787 |
+
# Don't check it out at first - nonetheless it will create a local
|
| 788 |
+
# branch according to the remote-HEAD if possible.
|
| 789 |
+
progress.update(
|
| 790 |
+
BEGIN | CLONE,
|
| 791 |
+
0,
|
| 792 |
+
1,
|
| 793 |
+
prefix
|
| 794 |
+
+ "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
|
| 795 |
+
)
|
| 796 |
+
if not dry_run:
|
| 797 |
+
mrepo = self._clone_repo(
|
| 798 |
+
self.repo,
|
| 799 |
+
self.url,
|
| 800 |
+
self.path,
|
| 801 |
+
self.name,
|
| 802 |
+
n=True,
|
| 803 |
+
env=env,
|
| 804 |
+
multi_options=clone_multi_options,
|
| 805 |
+
allow_unsafe_options=allow_unsafe_options,
|
| 806 |
+
allow_unsafe_protocols=allow_unsafe_protocols,
|
| 807 |
+
)
|
| 808 |
+
# END handle dry-run
|
| 809 |
+
progress.update(
|
| 810 |
+
END | CLONE,
|
| 811 |
+
0,
|
| 812 |
+
1,
|
| 813 |
+
prefix + "Done cloning to %s" % checkout_module_abspath,
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
if not dry_run:
|
| 817 |
+
# See whether we have a valid branch to check out.
|
| 818 |
+
try:
|
| 819 |
+
mrepo = cast("Repo", mrepo)
|
| 820 |
+
# Find a remote which has our branch - we try to be flexible.
|
| 821 |
+
remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
|
| 822 |
+
local_branch = mkhead(mrepo, self.branch_path)
|
| 823 |
+
|
| 824 |
+
# Have a valid branch, but no checkout - make sure we can figure
|
| 825 |
+
# that out by marking the commit with a null_sha.
|
| 826 |
+
local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
|
| 827 |
+
# END initial checkout + branch creation
|
| 828 |
+
|
| 829 |
+
# Make sure HEAD is not detached.
|
| 830 |
+
mrepo.head.set_reference(
|
| 831 |
+
local_branch,
|
| 832 |
+
logmsg="submodule: attaching head to %s" % local_branch,
|
| 833 |
+
)
|
| 834 |
+
mrepo.head.reference.set_tracking_branch(remote_branch)
|
| 835 |
+
except (IndexError, InvalidGitRepositoryError):
|
| 836 |
+
_logger.warning("Failed to checkout tracking branch %s", self.branch_path)
|
| 837 |
+
# END handle tracking branch
|
| 838 |
+
|
| 839 |
+
# NOTE: Have to write the repo config file as well, otherwise the
|
| 840 |
+
# default implementation will be offended and not update the
|
| 841 |
+
# repository. Maybe this is a good way to ensure it doesn't get into
|
| 842 |
+
# our way, but we want to stay backwards compatible too... It's so
|
| 843 |
+
# redundant!
|
| 844 |
+
with self.repo.config_writer() as writer:
|
| 845 |
+
writer.set_value(sm_section(self.name), "url", self.url)
|
| 846 |
+
# END handle dry_run
|
| 847 |
+
# END handle initialization
|
| 848 |
+
|
| 849 |
+
# DETERMINE SHAS TO CHECK OUT
|
| 850 |
+
#############################
|
| 851 |
+
binsha = self.binsha
|
| 852 |
+
hexsha = self.hexsha
|
| 853 |
+
if mrepo is not None:
|
| 854 |
+
# mrepo is only set if we are not in dry-run mode or if the module
|
| 855 |
+
# existed.
|
| 856 |
+
is_detached = mrepo.head.is_detached
|
| 857 |
+
# END handle dry_run
|
| 858 |
+
|
| 859 |
+
if mrepo is not None and to_latest_revision:
|
| 860 |
+
msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir
|
| 861 |
+
if not is_detached:
|
| 862 |
+
rref = mrepo.head.reference.tracking_branch()
|
| 863 |
+
if rref is not None:
|
| 864 |
+
rcommit = rref.commit
|
| 865 |
+
binsha = rcommit.binsha
|
| 866 |
+
hexsha = rcommit.hexsha
|
| 867 |
+
else:
|
| 868 |
+
_logger.error(
|
| 869 |
+
"%s a tracking branch was not set for local branch '%s'",
|
| 870 |
+
msg_base,
|
| 871 |
+
mrepo.head.reference,
|
| 872 |
+
)
|
| 873 |
+
# END handle remote ref
|
| 874 |
+
else:
|
| 875 |
+
_logger.error("%s there was no local tracking branch", msg_base)
|
| 876 |
+
# END handle detached head
|
| 877 |
+
# END handle to_latest_revision option
|
| 878 |
+
|
| 879 |
+
# Update the working tree.
|
| 880 |
+
# Handles dry_run.
|
| 881 |
+
if mrepo is not None and mrepo.head.commit.binsha != binsha:
|
| 882 |
+
# We must ensure that our destination sha (the one to point to) is in
|
| 883 |
+
# the future of our current head. Otherwise, we will reset changes that
|
| 884 |
+
# might have been done on the submodule, but were not yet pushed. We
|
| 885 |
+
# also handle the case that history has been rewritten, leaving no
|
| 886 |
+
# merge-base. In that case we behave conservatively, protecting possible
|
| 887 |
+
# changes the user had done.
|
| 888 |
+
may_reset = True
|
| 889 |
+
if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
|
| 890 |
+
base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
|
| 891 |
+
if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha):
|
| 892 |
+
if force:
|
| 893 |
+
msg = "Will force checkout or reset on local branch that is possibly in the future of"
|
| 894 |
+
msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
|
| 895 |
+
_logger.debug(msg)
|
| 896 |
+
else:
|
| 897 |
+
msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
|
| 898 |
+
msg %= (
|
| 899 |
+
is_detached and "checkout" or "reset",
|
| 900 |
+
mrepo.head,
|
| 901 |
+
mrepo,
|
| 902 |
+
)
|
| 903 |
+
_logger.info(msg)
|
| 904 |
+
may_reset = False
|
| 905 |
+
# END handle force
|
| 906 |
+
# END handle if we are in the future
|
| 907 |
+
|
| 908 |
+
if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
|
| 909 |
+
raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
|
| 910 |
+
# END handle force and dirty state
|
| 911 |
+
# END handle empty repo
|
| 912 |
+
|
| 913 |
+
# END verify future/past
|
| 914 |
+
progress.update(
|
| 915 |
+
BEGIN | UPDWKTREE,
|
| 916 |
+
0,
|
| 917 |
+
1,
|
| 918 |
+
prefix
|
| 919 |
+
+ "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha),
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
if not dry_run and may_reset:
|
| 923 |
+
if is_detached:
|
| 924 |
+
# NOTE: For now we force. The user is not supposed to change
|
| 925 |
+
# detached submodules anyway. Maybe at some point this becomes
|
| 926 |
+
# an option, to properly handle user modifications - see below
|
| 927 |
+
# for future options regarding rebase and merge.
|
| 928 |
+
mrepo.git.checkout(hexsha, force=force)
|
| 929 |
+
else:
|
| 930 |
+
mrepo.head.reset(hexsha, index=True, working_tree=True)
|
| 931 |
+
# END handle checkout
|
| 932 |
+
# If we may reset/checkout.
|
| 933 |
+
progress.update(
|
| 934 |
+
END | UPDWKTREE,
|
| 935 |
+
0,
|
| 936 |
+
1,
|
| 937 |
+
prefix + "Done updating working tree for submodule %r" % self.name,
|
| 938 |
+
)
|
| 939 |
+
# END update to new commit only if needed
|
| 940 |
+
except Exception as err:
|
| 941 |
+
if not keep_going:
|
| 942 |
+
raise
|
| 943 |
+
_logger.error(str(err))
|
| 944 |
+
# END handle keep_going
|
| 945 |
+
|
| 946 |
+
# HANDLE RECURSION
|
| 947 |
+
##################
|
| 948 |
+
if recursive:
|
| 949 |
+
# In dry_run mode, the module might not exist.
|
| 950 |
+
if mrepo is not None:
|
| 951 |
+
for submodule in self.iter_items(self.module()):
|
| 952 |
+
submodule.update(
|
| 953 |
+
recursive,
|
| 954 |
+
init,
|
| 955 |
+
to_latest_revision,
|
| 956 |
+
progress=progress,
|
| 957 |
+
dry_run=dry_run,
|
| 958 |
+
force=force,
|
| 959 |
+
keep_going=keep_going,
|
| 960 |
+
)
|
| 961 |
+
# END handle recursive update
|
| 962 |
+
# END handle dry run
|
| 963 |
+
# END for each submodule
|
| 964 |
+
|
| 965 |
+
return self
|
| 966 |
+
|
| 967 |
+
@unbare_repo
|
| 968 |
+
def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule":
|
| 969 |
+
"""Move the submodule to a another module path. This involves physically moving
|
| 970 |
+
the repository at our current path, changing the configuration, as well as
|
| 971 |
+
adjusting our index entry accordingly.
|
| 972 |
+
|
| 973 |
+
:param module_path:
|
| 974 |
+
The path to which to move our module in the parent repository's working
|
| 975 |
+
tree, given as repository-relative or absolute path. Intermediate
|
| 976 |
+
directories will be created accordingly. If the path already exists, it must
|
| 977 |
+
be empty. Trailing (back)slashes are removed automatically.
|
| 978 |
+
|
| 979 |
+
:param configuration:
|
| 980 |
+
If ``True``, the configuration will be adjusted to let the submodule point
|
| 981 |
+
to the given path.
|
| 982 |
+
|
| 983 |
+
:param module:
|
| 984 |
+
If ``True``, the repository managed by this submodule will be moved as well.
|
| 985 |
+
If ``False``, we don't move the submodule's checkout, which may leave the
|
| 986 |
+
parent repository in an inconsistent state.
|
| 987 |
+
|
| 988 |
+
:return:
|
| 989 |
+
self
|
| 990 |
+
|
| 991 |
+
:raise ValueError:
|
| 992 |
+
If the module path existed and was not empty, or was a file.
|
| 993 |
+
|
| 994 |
+
:note:
|
| 995 |
+
Currently the method is not atomic, and it could leave the repository in an
|
| 996 |
+
inconsistent state if a sub-step fails for some reason.
|
| 997 |
+
"""
|
| 998 |
+
if module + configuration < 1:
|
| 999 |
+
raise ValueError("You must specify to move at least the module or the configuration of the submodule")
|
| 1000 |
+
# END handle input
|
| 1001 |
+
|
| 1002 |
+
module_checkout_path = self._to_relative_path(self.repo, module_path)
|
| 1003 |
+
|
| 1004 |
+
# VERIFY DESTINATION
|
| 1005 |
+
if module_checkout_path == self.path:
|
| 1006 |
+
return self
|
| 1007 |
+
# END handle no change
|
| 1008 |
+
|
| 1009 |
+
module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path)
|
| 1010 |
+
if osp.isfile(module_checkout_abspath):
|
| 1011 |
+
raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
|
| 1012 |
+
# END handle target files
|
| 1013 |
+
|
| 1014 |
+
index = self.repo.index
|
| 1015 |
+
tekey = index.entry_key(module_checkout_path, 0)
|
| 1016 |
+
# if the target item already exists, fail
|
| 1017 |
+
if configuration and tekey in index.entries:
|
| 1018 |
+
raise ValueError("Index entry for target path did already exist")
|
| 1019 |
+
# END handle index key already there
|
| 1020 |
+
|
| 1021 |
+
# Remove existing destination.
|
| 1022 |
+
if module:
|
| 1023 |
+
if osp.exists(module_checkout_abspath):
|
| 1024 |
+
if len(os.listdir(module_checkout_abspath)):
|
| 1025 |
+
raise ValueError("Destination module directory was not empty")
|
| 1026 |
+
# END handle non-emptiness
|
| 1027 |
+
|
| 1028 |
+
if osp.islink(module_checkout_abspath):
|
| 1029 |
+
os.remove(module_checkout_abspath)
|
| 1030 |
+
else:
|
| 1031 |
+
os.rmdir(module_checkout_abspath)
|
| 1032 |
+
# END handle link
|
| 1033 |
+
else:
|
| 1034 |
+
# Recreate parent directories.
|
| 1035 |
+
# NOTE: renames() does that now.
|
| 1036 |
+
pass
|
| 1037 |
+
# END handle existence
|
| 1038 |
+
# END handle module
|
| 1039 |
+
|
| 1040 |
+
# Move the module into place if possible.
|
| 1041 |
+
cur_path = self.abspath
|
| 1042 |
+
renamed_module = False
|
| 1043 |
+
if module and osp.exists(cur_path):
|
| 1044 |
+
os.renames(cur_path, module_checkout_abspath)
|
| 1045 |
+
renamed_module = True
|
| 1046 |
+
|
| 1047 |
+
if osp.isfile(osp.join(module_checkout_abspath, ".git")):
|
| 1048 |
+
module_abspath = self._module_abspath(self.repo, self.path, self.name)
|
| 1049 |
+
self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
|
| 1050 |
+
# END handle git file rewrite
|
| 1051 |
+
# END move physical module
|
| 1052 |
+
|
| 1053 |
+
# Rename the index entry - we have to manipulate the index directly as git-mv
|
| 1054 |
+
# cannot be used on submodules... yeah.
|
| 1055 |
+
previous_sm_path = self.path
|
| 1056 |
+
try:
|
| 1057 |
+
if configuration:
|
| 1058 |
+
try:
|
| 1059 |
+
ekey = index.entry_key(self.path, 0)
|
| 1060 |
+
entry = index.entries[ekey]
|
| 1061 |
+
del index.entries[ekey]
|
| 1062 |
+
nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
|
| 1063 |
+
index.entries[tekey] = nentry
|
| 1064 |
+
except KeyError as e:
|
| 1065 |
+
raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e
|
| 1066 |
+
# END handle submodule doesn't exist
|
| 1067 |
+
|
| 1068 |
+
# Update configuration.
|
| 1069 |
+
with self.config_writer(index=index) as writer: # Auto-write.
|
| 1070 |
+
writer.set_value("path", module_checkout_path)
|
| 1071 |
+
self.path = module_checkout_path
|
| 1072 |
+
# END handle configuration flag
|
| 1073 |
+
except Exception:
|
| 1074 |
+
if renamed_module:
|
| 1075 |
+
os.renames(module_checkout_abspath, cur_path)
|
| 1076 |
+
# END undo module renaming
|
| 1077 |
+
raise
|
| 1078 |
+
# END handle undo rename
|
| 1079 |
+
|
| 1080 |
+
# Auto-rename submodule if its name was 'default', that is, the checkout
|
| 1081 |
+
# directory.
|
| 1082 |
+
if previous_sm_path == self.name:
|
| 1083 |
+
self.rename(module_checkout_path)
|
| 1084 |
+
|
| 1085 |
+
return self
|
| 1086 |
+
|
| 1087 |
+
@unbare_repo
|
| 1088 |
+
def remove(
|
| 1089 |
+
self,
|
| 1090 |
+
module: bool = True,
|
| 1091 |
+
force: bool = False,
|
| 1092 |
+
configuration: bool = True,
|
| 1093 |
+
dry_run: bool = False,
|
| 1094 |
+
) -> "Submodule":
|
| 1095 |
+
"""Remove this submodule from the repository. This will remove our entry
|
| 1096 |
+
from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
|
| 1097 |
+
|
| 1098 |
+
:param module:
|
| 1099 |
+
If ``True``, the checked out module we point to will be deleted as well. If
|
| 1100 |
+
that module is currently on a commit outside any branch in the remote, or if
|
| 1101 |
+
it is ahead of its tracking branch, or if there are modified or untracked
|
| 1102 |
+
files in its working tree, then the removal will fail. In case the removal
|
| 1103 |
+
of the repository fails for these reasons, the submodule status will not
|
| 1104 |
+
have been altered.
|
| 1105 |
+
|
| 1106 |
+
If this submodule has child modules of its own, these will be deleted prior
|
| 1107 |
+
to touching the direct submodule.
|
| 1108 |
+
|
| 1109 |
+
:param force:
|
| 1110 |
+
Enforces the deletion of the module even though it contains modifications.
|
| 1111 |
+
This basically enforces a brute-force file system based deletion.
|
| 1112 |
+
|
| 1113 |
+
:param configuration:
|
| 1114 |
+
If ``True``, the submodule is deleted from the configuration, otherwise it
|
| 1115 |
+
isn't. Although this should be enabled most of the time, this flag enables
|
| 1116 |
+
you to safely delete the repository of your submodule.
|
| 1117 |
+
|
| 1118 |
+
:param dry_run:
|
| 1119 |
+
If ``True``, we will not actually do anything, but throw the errors we would
|
| 1120 |
+
usually throw.
|
| 1121 |
+
|
| 1122 |
+
:return:
|
| 1123 |
+
self
|
| 1124 |
+
|
| 1125 |
+
:note:
|
| 1126 |
+
Doesn't work in bare repositories.
|
| 1127 |
+
|
| 1128 |
+
:note:
|
| 1129 |
+
Doesn't work atomically, as failure to remove any part of the submodule will
|
| 1130 |
+
leave an inconsistent state.
|
| 1131 |
+
|
| 1132 |
+
:raise git.exc.InvalidGitRepositoryError:
|
| 1133 |
+
Thrown if the repository cannot be deleted.
|
| 1134 |
+
|
| 1135 |
+
:raise OSError:
|
| 1136 |
+
If directories or files could not be removed.
|
| 1137 |
+
"""
|
| 1138 |
+
if not (module or configuration):
|
| 1139 |
+
raise ValueError("Need to specify to delete at least the module, or the configuration")
|
| 1140 |
+
# END handle parameters
|
| 1141 |
+
|
| 1142 |
+
# Recursively remove children of this submodule.
|
| 1143 |
+
nc = 0
|
| 1144 |
+
for csm in self.children():
|
| 1145 |
+
nc += 1
|
| 1146 |
+
csm.remove(module, force, configuration, dry_run)
|
| 1147 |
+
del csm
|
| 1148 |
+
|
| 1149 |
+
if configuration and not dry_run and nc > 0:
|
| 1150 |
+
# Ensure we don't leave the parent repository in a dirty state, and commit
|
| 1151 |
+
# our changes. It's important for recursive, unforced, deletions to work as
|
| 1152 |
+
# expected.
|
| 1153 |
+
self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
|
| 1154 |
+
# END handle recursion
|
| 1155 |
+
|
| 1156 |
+
# DELETE REPOSITORY WORKING TREE
|
| 1157 |
+
################################
|
| 1158 |
+
if module and self.module_exists():
|
| 1159 |
+
mod = self.module()
|
| 1160 |
+
git_dir = mod.git_dir
|
| 1161 |
+
if force:
|
| 1162 |
+
# Take the fast lane and just delete everything in our module path.
|
| 1163 |
+
# TODO: If we run into permission problems, we have a highly
|
| 1164 |
+
# inconsistent state. Delete the .git folders last, start with the
|
| 1165 |
+
# submodules first.
|
| 1166 |
+
mp = self.abspath
|
| 1167 |
+
method: Union[None, Callable[[PathLike], None]] = None
|
| 1168 |
+
if osp.islink(mp):
|
| 1169 |
+
method = os.remove
|
| 1170 |
+
elif osp.isdir(mp):
|
| 1171 |
+
method = rmtree
|
| 1172 |
+
elif osp.exists(mp):
|
| 1173 |
+
raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
|
| 1174 |
+
# END handle brutal deletion
|
| 1175 |
+
if not dry_run:
|
| 1176 |
+
assert method
|
| 1177 |
+
method(mp)
|
| 1178 |
+
# END apply deletion method
|
| 1179 |
+
else:
|
| 1180 |
+
# Verify we may delete our module.
|
| 1181 |
+
if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
|
| 1182 |
+
raise InvalidGitRepositoryError(
|
| 1183 |
+
"Cannot delete module at %s with any modifications, unless force is specified"
|
| 1184 |
+
% mod.working_tree_dir
|
| 1185 |
+
)
|
| 1186 |
+
# END check for dirt
|
| 1187 |
+
|
| 1188 |
+
# Figure out whether we have new commits compared to the remotes.
|
| 1189 |
+
# NOTE: If the user pulled all the time, the remote heads might not have
|
| 1190 |
+
# been updated, so commits coming from the remote look as if they come
|
| 1191 |
+
# from us. But we stay strictly read-only and don't fetch beforehand.
|
| 1192 |
+
for remote in mod.remotes:
|
| 1193 |
+
num_branches_with_new_commits = 0
|
| 1194 |
+
rrefs = remote.refs
|
| 1195 |
+
for rref in rrefs:
|
| 1196 |
+
num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
|
| 1197 |
+
# END for each remote ref
|
| 1198 |
+
# Not a single remote branch contained all our commits.
|
| 1199 |
+
if len(rrefs) and num_branches_with_new_commits == len(rrefs):
|
| 1200 |
+
raise InvalidGitRepositoryError(
|
| 1201 |
+
"Cannot delete module at %s as there are new commits" % mod.working_tree_dir
|
| 1202 |
+
)
|
| 1203 |
+
# END handle new commits
|
| 1204 |
+
# We have to manually delete some references to allow resources to
|
| 1205 |
+
# be cleaned up immediately when we are done with them, because
|
| 1206 |
+
# Python's scoping is no more granular than the whole function (loop
|
| 1207 |
+
# bodies are not scopes). When the objects stay alive longer, they
|
| 1208 |
+
# can keep handles open. On Windows, this is a problem.
|
| 1209 |
+
if len(rrefs):
|
| 1210 |
+
del rref # skipcq: PYL-W0631
|
| 1211 |
+
# END handle remotes
|
| 1212 |
+
del rrefs
|
| 1213 |
+
del remote
|
| 1214 |
+
# END for each remote
|
| 1215 |
+
|
| 1216 |
+
# Finally delete our own submodule.
|
| 1217 |
+
if not dry_run:
|
| 1218 |
+
self._clear_cache()
|
| 1219 |
+
wtd = mod.working_tree_dir
|
| 1220 |
+
del mod # Release file-handles (Windows).
|
| 1221 |
+
gc.collect()
|
| 1222 |
+
rmtree(str(wtd))
|
| 1223 |
+
# END delete tree if possible
|
| 1224 |
+
# END handle force
|
| 1225 |
+
|
| 1226 |
+
if not dry_run and osp.isdir(git_dir):
|
| 1227 |
+
self._clear_cache()
|
| 1228 |
+
rmtree(git_dir)
|
| 1229 |
+
# END handle separate bare repository
|
| 1230 |
+
# END handle module deletion
|
| 1231 |
+
|
| 1232 |
+
# Void our data so as not to delay invalid access.
|
| 1233 |
+
if not dry_run:
|
| 1234 |
+
self._clear_cache()
|
| 1235 |
+
|
| 1236 |
+
# DELETE CONFIGURATION
|
| 1237 |
+
######################
|
| 1238 |
+
if configuration and not dry_run:
|
| 1239 |
+
# First the index-entry.
|
| 1240 |
+
parent_index = self.repo.index
|
| 1241 |
+
try:
|
| 1242 |
+
del parent_index.entries[parent_index.entry_key(self.path, 0)]
|
| 1243 |
+
except KeyError:
|
| 1244 |
+
pass
|
| 1245 |
+
# END delete entry
|
| 1246 |
+
parent_index.write()
|
| 1247 |
+
|
| 1248 |
+
# Now git config - we need the config intact, otherwise we can't query
|
| 1249 |
+
# information anymore.
|
| 1250 |
+
|
| 1251 |
+
with self.repo.config_writer() as gcp_writer:
|
| 1252 |
+
gcp_writer.remove_section(sm_section(self.name))
|
| 1253 |
+
|
| 1254 |
+
with self.config_writer() as sc_writer:
|
| 1255 |
+
sc_writer.remove_section()
|
| 1256 |
+
# END delete configuration
|
| 1257 |
+
|
| 1258 |
+
return self
|
| 1259 |
+
|
| 1260 |
+
def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
|
| 1261 |
+
"""Set this instance to use the given commit whose tree is supposed to
|
| 1262 |
+
contain the ``.gitmodules`` blob.
|
| 1263 |
+
|
| 1264 |
+
:param commit:
|
| 1265 |
+
Commit-ish reference pointing at the root tree, or ``None`` to always point
|
| 1266 |
+
to the most recent commit.
|
| 1267 |
+
|
| 1268 |
+
:param check:
|
| 1269 |
+
If ``True``, relatively expensive checks will be performed to verify
|
| 1270 |
+
validity of the submodule.
|
| 1271 |
+
|
| 1272 |
+
:raise ValueError:
|
| 1273 |
+
If the commit's tree didn't contain the ``.gitmodules`` blob.
|
| 1274 |
+
|
| 1275 |
+
:raise ValueError:
|
| 1276 |
+
If the parent commit didn't store this submodule under the current path.
|
| 1277 |
+
|
| 1278 |
+
:return:
|
| 1279 |
+
self
|
| 1280 |
+
"""
|
| 1281 |
+
if commit is None:
|
| 1282 |
+
self._parent_commit = None
|
| 1283 |
+
return self
|
| 1284 |
+
# END handle None
|
| 1285 |
+
pcommit = self.repo.commit(commit)
|
| 1286 |
+
pctree = pcommit.tree
|
| 1287 |
+
if self.k_modules_file not in pctree:
|
| 1288 |
+
raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file))
|
| 1289 |
+
# END handle exceptions
|
| 1290 |
+
|
| 1291 |
+
prev_pc = self._parent_commit
|
| 1292 |
+
self._parent_commit = pcommit
|
| 1293 |
+
|
| 1294 |
+
if check:
|
| 1295 |
+
parser = self._config_parser(self.repo, self._parent_commit, read_only=True)
|
| 1296 |
+
if not parser.has_section(sm_section(self.name)):
|
| 1297 |
+
self._parent_commit = prev_pc
|
| 1298 |
+
raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit))
|
| 1299 |
+
# END handle submodule did not exist
|
| 1300 |
+
# END handle checking mode
|
| 1301 |
+
|
| 1302 |
+
# Update our sha, it could have changed.
|
| 1303 |
+
# If check is False, we might see a parent-commit that doesn't even contain the
|
| 1304 |
+
# submodule anymore. in that case, mark our sha as being NULL.
|
| 1305 |
+
try:
|
| 1306 |
+
self.binsha = pctree[str(self.path)].binsha
|
| 1307 |
+
except KeyError:
|
| 1308 |
+
self.binsha = self.NULL_BIN_SHA
|
| 1309 |
+
|
| 1310 |
+
self._clear_cache()
|
| 1311 |
+
return self
|
| 1312 |
+
|
| 1313 |
+
@unbare_repo
|
| 1314 |
+
def config_writer(
|
| 1315 |
+
self, index: Union["IndexFile", None] = None, write: bool = True
|
| 1316 |
+
) -> SectionConstraint["SubmoduleConfigParser"]:
|
| 1317 |
+
"""
|
| 1318 |
+
:return:
|
| 1319 |
+
A config writer instance allowing you to read and write the data belonging
|
| 1320 |
+
to this submodule into the ``.gitmodules`` file.
|
| 1321 |
+
|
| 1322 |
+
:param index:
|
| 1323 |
+
If not ``None``, an :class:`~git.index.base.IndexFile` instance which should
|
| 1324 |
+
be written. Defaults to the index of the :class:`Submodule`'s parent
|
| 1325 |
+
repository.
|
| 1326 |
+
|
| 1327 |
+
:param write:
|
| 1328 |
+
If ``True``, the index will be written each time a configuration value changes.
|
| 1329 |
+
|
| 1330 |
+
:note:
|
| 1331 |
+
The parameters allow for a more efficient writing of the index, as you can
|
| 1332 |
+
pass in a modified index on your own, prevent automatic writing, and write
|
| 1333 |
+
yourself once the whole operation is complete.
|
| 1334 |
+
|
| 1335 |
+
:raise ValueError:
|
| 1336 |
+
If trying to get a writer on a parent_commit which does not match the
|
| 1337 |
+
current head commit.
|
| 1338 |
+
|
| 1339 |
+
:raise IOError:
|
| 1340 |
+
If the ``.gitmodules`` file/blob could not be read.
|
| 1341 |
+
"""
|
| 1342 |
+
writer = self._config_parser_constrained(read_only=False)
|
| 1343 |
+
if index is not None:
|
| 1344 |
+
writer.config._index = index
|
| 1345 |
+
writer.config._auto_write = write
|
| 1346 |
+
return writer
|
| 1347 |
+
|
| 1348 |
+
@unbare_repo
|
| 1349 |
+
def rename(self, new_name: str) -> "Submodule":
|
| 1350 |
+
"""Rename this submodule.
|
| 1351 |
+
|
| 1352 |
+
:note:
|
| 1353 |
+
This method takes care of renaming the submodule in various places, such as:
|
| 1354 |
+
|
| 1355 |
+
* ``$parent_git_dir / config``
|
| 1356 |
+
* ``$working_tree_dir / .gitmodules``
|
| 1357 |
+
* (git >= v1.8.0: move submodule repository to new name)
|
| 1358 |
+
|
| 1359 |
+
As ``.gitmodules`` will be changed, you would need to make a commit afterwards.
|
| 1360 |
+
The changed ``.gitmodules`` file will already be added to the index.
|
| 1361 |
+
|
| 1362 |
+
:return:
|
| 1363 |
+
This :class:`Submodule` instance
|
| 1364 |
+
"""
|
| 1365 |
+
if self.name == new_name:
|
| 1366 |
+
return self
|
| 1367 |
+
|
| 1368 |
+
# .git/config
|
| 1369 |
+
with self.repo.config_writer() as pw:
|
| 1370 |
+
# As we ourselves didn't write anything about submodules into the parent
|
| 1371 |
+
# .git/config, we will not require it to exist, and just ignore missing
|
| 1372 |
+
# entries.
|
| 1373 |
+
if pw.has_section(sm_section(self.name)):
|
| 1374 |
+
pw.rename_section(sm_section(self.name), sm_section(new_name))
|
| 1375 |
+
|
| 1376 |
+
# .gitmodules
|
| 1377 |
+
with self.config_writer(write=True).config as cw:
|
| 1378 |
+
cw.rename_section(sm_section(self.name), sm_section(new_name))
|
| 1379 |
+
|
| 1380 |
+
self._name = new_name
|
| 1381 |
+
|
| 1382 |
+
# .git/modules
|
| 1383 |
+
mod = self.module()
|
| 1384 |
+
if mod.has_separate_working_tree():
|
| 1385 |
+
destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
|
| 1386 |
+
source_dir = mod.git_dir
|
| 1387 |
+
# Let's be sure the submodule name is not so obviously tied to a directory.
|
| 1388 |
+
if str(destination_module_abspath).startswith(str(mod.git_dir)):
|
| 1389 |
+
tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
|
| 1390 |
+
os.renames(source_dir, tmp_dir)
|
| 1391 |
+
source_dir = tmp_dir
|
| 1392 |
+
# END handle self-containment
|
| 1393 |
+
os.renames(source_dir, destination_module_abspath)
|
| 1394 |
+
if mod.working_tree_dir:
|
| 1395 |
+
self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
|
| 1396 |
+
# END move separate git repository
|
| 1397 |
+
|
| 1398 |
+
return self
|
| 1399 |
+
|
| 1400 |
+
# } END edit interface
|
| 1401 |
+
|
| 1402 |
+
# { Query Interface
|
| 1403 |
+
|
| 1404 |
+
@unbare_repo
|
| 1405 |
+
def module(self) -> "Repo":
|
| 1406 |
+
"""
|
| 1407 |
+
:return:
|
| 1408 |
+
:class:`~git.repo.base.Repo` instance initialized from the repository at our
|
| 1409 |
+
submodule path
|
| 1410 |
+
|
| 1411 |
+
:raise git.exc.InvalidGitRepositoryError:
|
| 1412 |
+
If a repository was not available.
|
| 1413 |
+
This could also mean that it was not yet initialized.
|
| 1414 |
+
"""
|
| 1415 |
+
module_checkout_abspath = self.abspath
|
| 1416 |
+
try:
|
| 1417 |
+
repo = git.Repo(module_checkout_abspath)
|
| 1418 |
+
if repo != self.repo:
|
| 1419 |
+
return repo
|
| 1420 |
+
# END handle repo uninitialized
|
| 1421 |
+
except (InvalidGitRepositoryError, NoSuchPathError) as e:
|
| 1422 |
+
raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e
|
| 1423 |
+
else:
|
| 1424 |
+
raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath)
|
| 1425 |
+
# END handle exceptions
|
| 1426 |
+
|
| 1427 |
+
def module_exists(self) -> bool:
|
| 1428 |
+
"""
|
| 1429 |
+
:return:
|
| 1430 |
+
``True`` if our module exists and is a valid git repository.
|
| 1431 |
+
See the :meth:`module` method.
|
| 1432 |
+
"""
|
| 1433 |
+
try:
|
| 1434 |
+
self.module()
|
| 1435 |
+
return True
|
| 1436 |
+
except Exception:
|
| 1437 |
+
return False
|
| 1438 |
+
# END handle exception
|
| 1439 |
+
|
| 1440 |
+
def exists(self) -> bool:
|
| 1441 |
+
"""
|
| 1442 |
+
:return:
|
| 1443 |
+
``True`` if the submodule exists, ``False`` otherwise.
|
| 1444 |
+
Please note that a submodule may exist (in the ``.gitmodules`` file) even
|
| 1445 |
+
though its module doesn't exist on disk.
|
| 1446 |
+
"""
|
| 1447 |
+
# Keep attributes for later, and restore them if we have no valid data.
|
| 1448 |
+
# This way we do not actually alter the state of the object.
|
| 1449 |
+
loc = locals()
|
| 1450 |
+
for attr in self._cache_attrs:
|
| 1451 |
+
try:
|
| 1452 |
+
if hasattr(self, attr):
|
| 1453 |
+
loc[attr] = getattr(self, attr)
|
| 1454 |
+
# END if we have the attribute cache
|
| 1455 |
+
except (cp.NoSectionError, ValueError):
|
| 1456 |
+
# On PY3, this can happen apparently... don't know why this doesn't
|
| 1457 |
+
# happen on PY2.
|
| 1458 |
+
pass
|
| 1459 |
+
# END for each attr
|
| 1460 |
+
self._clear_cache()
|
| 1461 |
+
|
| 1462 |
+
try:
|
| 1463 |
+
try:
|
| 1464 |
+
self.path # noqa: B018
|
| 1465 |
+
return True
|
| 1466 |
+
except Exception:
|
| 1467 |
+
return False
|
| 1468 |
+
# END handle exceptions
|
| 1469 |
+
finally:
|
| 1470 |
+
for attr in self._cache_attrs:
|
| 1471 |
+
if attr in loc:
|
| 1472 |
+
setattr(self, attr, loc[attr])
|
| 1473 |
+
# END if we have a cache
|
| 1474 |
+
# END reapply each attribute
|
| 1475 |
+
# END handle object state consistency
|
| 1476 |
+
|
| 1477 |
+
@property
|
| 1478 |
+
def branch(self) -> "Head":
|
| 1479 |
+
"""
|
| 1480 |
+
:return:
|
| 1481 |
+
The branch instance that we are to checkout
|
| 1482 |
+
|
| 1483 |
+
:raise git.exc.InvalidGitRepositoryError:
|
| 1484 |
+
If our module is not yet checked out.
|
| 1485 |
+
"""
|
| 1486 |
+
return mkhead(self.module(), self._branch_path)
|
| 1487 |
+
|
| 1488 |
+
@property
|
| 1489 |
+
def branch_path(self) -> PathLike:
|
| 1490 |
+
"""
|
| 1491 |
+
:return:
|
| 1492 |
+
Full repository-relative path as string to the branch we would checkout from
|
| 1493 |
+
the remote and track
|
| 1494 |
+
"""
|
| 1495 |
+
return self._branch_path
|
| 1496 |
+
|
| 1497 |
+
@property
|
| 1498 |
+
def branch_name(self) -> str:
|
| 1499 |
+
"""
|
| 1500 |
+
:return:
|
| 1501 |
+
The name of the branch, which is the shortest possible branch name
|
| 1502 |
+
"""
|
| 1503 |
+
# Use an instance method, for this we create a temporary Head instance which
|
| 1504 |
+
# uses a repository that is available at least (it makes no difference).
|
| 1505 |
+
return git.Head(self.repo, self._branch_path).name
|
| 1506 |
+
|
| 1507 |
+
@property
|
| 1508 |
+
def url(self) -> str:
|
| 1509 |
+
""":return: The url to the repository our submodule's repository refers to"""
|
| 1510 |
+
return self._url
|
| 1511 |
+
|
| 1512 |
+
@property
|
| 1513 |
+
def parent_commit(self) -> "Commit":
|
| 1514 |
+
"""
|
| 1515 |
+
:return:
|
| 1516 |
+
:class:`~git.objects.commit.Commit` instance with the tree containing the
|
| 1517 |
+
``.gitmodules`` file
|
| 1518 |
+
|
| 1519 |
+
:note:
|
| 1520 |
+
Will always point to the current head's commit if it was not set explicitly.
|
| 1521 |
+
"""
|
| 1522 |
+
if self._parent_commit is None:
|
| 1523 |
+
return self.repo.commit()
|
| 1524 |
+
return self._parent_commit
|
| 1525 |
+
|
| 1526 |
+
@property
|
| 1527 |
+
def name(self) -> str:
|
| 1528 |
+
"""
|
| 1529 |
+
:return:
|
| 1530 |
+
The name of this submodule. It is used to identify it within the
|
| 1531 |
+
``.gitmodules`` file.
|
| 1532 |
+
|
| 1533 |
+
:note:
|
| 1534 |
+
By default, this is the name is the path at which to find the submodule, but
|
| 1535 |
+
in GitPython it should be a unique identifier similar to the identifiers
|
| 1536 |
+
used for remotes, which allows to change the path of the submodule easily.
|
| 1537 |
+
"""
|
| 1538 |
+
return self._name
|
| 1539 |
+
|
| 1540 |
+
def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
|
| 1541 |
+
"""
|
| 1542 |
+
:return:
|
| 1543 |
+
ConfigReader instance which allows you to query the configuration values of
|
| 1544 |
+
this submodule, as provided by the ``.gitmodules`` file.
|
| 1545 |
+
|
| 1546 |
+
:note:
|
| 1547 |
+
The config reader will actually read the data directly from the repository
|
| 1548 |
+
and thus does not need nor care about your working tree.
|
| 1549 |
+
|
| 1550 |
+
:note:
|
| 1551 |
+
Should be cached by the caller and only kept as long as needed.
|
| 1552 |
+
|
| 1553 |
+
:raise IOError:
|
| 1554 |
+
If the ``.gitmodules`` file/blob could not be read.
|
| 1555 |
+
"""
|
| 1556 |
+
return self._config_parser_constrained(read_only=True)
|
| 1557 |
+
|
| 1558 |
+
def children(self) -> IterableList["Submodule"]:
|
| 1559 |
+
"""
|
| 1560 |
+
:return:
|
| 1561 |
+
IterableList(Submodule, ...) An iterable list of :class:`Submodule`
|
| 1562 |
+
instances which are children of this submodule or 0 if the submodule is not
|
| 1563 |
+
checked out.
|
| 1564 |
+
"""
|
| 1565 |
+
return self._get_intermediate_items(self)
|
| 1566 |
+
|
| 1567 |
+
# } END query interface
|
| 1568 |
+
|
| 1569 |
+
# { Iterable Interface
|
| 1570 |
+
|
| 1571 |
+
@classmethod
|
| 1572 |
+
def iter_items(
|
| 1573 |
+
cls,
|
| 1574 |
+
repo: "Repo",
|
| 1575 |
+
parent_commit: Union[Commit_ish, str] = "HEAD",
|
| 1576 |
+
*args: Any,
|
| 1577 |
+
**kwargs: Any,
|
| 1578 |
+
) -> Iterator["Submodule"]:
|
| 1579 |
+
"""
|
| 1580 |
+
:return:
|
| 1581 |
+
Iterator yielding :class:`Submodule` instances available in the given
|
| 1582 |
+
repository
|
| 1583 |
+
"""
|
| 1584 |
+
try:
|
| 1585 |
+
pc = repo.commit(parent_commit) # Parent commit instance
|
| 1586 |
+
parser = cls._config_parser(repo, pc, read_only=True)
|
| 1587 |
+
except (IOError, BadName):
|
| 1588 |
+
return
|
| 1589 |
+
# END handle empty iterator
|
| 1590 |
+
|
| 1591 |
+
for sms in parser.sections():
|
| 1592 |
+
n = sm_name(sms)
|
| 1593 |
+
p = parser.get(sms, "path")
|
| 1594 |
+
u = parser.get(sms, "url")
|
| 1595 |
+
b = cls.k_head_default
|
| 1596 |
+
if parser.has_option(sms, cls.k_head_option):
|
| 1597 |
+
b = str(parser.get(sms, cls.k_head_option))
|
| 1598 |
+
# END handle optional information
|
| 1599 |
+
|
| 1600 |
+
# Get the binsha.
|
| 1601 |
+
index = repo.index
|
| 1602 |
+
try:
|
| 1603 |
+
rt = pc.tree # Root tree
|
| 1604 |
+
sm = rt[p]
|
| 1605 |
+
except KeyError:
|
| 1606 |
+
# Try the index, maybe it was just added.
|
| 1607 |
+
try:
|
| 1608 |
+
entry = index.entries[index.entry_key(p, 0)]
|
| 1609 |
+
sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
|
| 1610 |
+
except KeyError:
|
| 1611 |
+
# The submodule doesn't exist, probably it wasn't removed from the
|
| 1612 |
+
# .gitmodules file.
|
| 1613 |
+
continue
|
| 1614 |
+
# END handle keyerror
|
| 1615 |
+
# END handle critical error
|
| 1616 |
+
|
| 1617 |
+
# Make sure we are looking at a submodule object.
|
| 1618 |
+
if type(sm) is not git.objects.submodule.base.Submodule:
|
| 1619 |
+
continue
|
| 1620 |
+
|
| 1621 |
+
# Fill in remaining info - saves time as it doesn't have to be parsed again.
|
| 1622 |
+
sm._name = n
|
| 1623 |
+
if pc != repo.commit():
|
| 1624 |
+
sm._parent_commit = pc
|
| 1625 |
+
# END set only if not most recent!
|
| 1626 |
+
sm._branch_path = git.Head.to_full_path(b)
|
| 1627 |
+
sm._url = u
|
| 1628 |
+
|
| 1629 |
+
yield sm
|
| 1630 |
+
# END for each section
|
| 1631 |
+
|
| 1632 |
+
# } END iterable interface
|
venv/lib/python3.10/site-packages/git/objects/submodule/root.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
__all__ = ["RootModule", "RootUpdateProgress"]
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
import git
|
| 9 |
+
from git.exc import InvalidGitRepositoryError
|
| 10 |
+
|
| 11 |
+
from .base import Submodule, UpdateProgress
|
| 12 |
+
from .util import find_first_remote_branch
|
| 13 |
+
|
| 14 |
+
# typing -------------------------------------------------------------------
|
| 15 |
+
|
| 16 |
+
from typing import TYPE_CHECKING, Union
|
| 17 |
+
|
| 18 |
+
from git.types import Commit_ish
|
| 19 |
+
|
| 20 |
+
if TYPE_CHECKING:
|
| 21 |
+
from git.repo import Repo
|
| 22 |
+
from git.util import IterableList
|
| 23 |
+
|
| 24 |
+
# ----------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
_logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class RootUpdateProgress(UpdateProgress):
|
| 30 |
+
"""Utility class which adds more opcodes to
|
| 31 |
+
:class:`~git.objects.submodule.base.UpdateProgress`."""
|
| 32 |
+
|
| 33 |
+
REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
|
| 34 |
+
1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
|
| 35 |
+
]
|
| 36 |
+
_num_op_codes = UpdateProgress._num_op_codes + 4
|
| 37 |
+
|
| 38 |
+
__slots__ = ()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
BEGIN = RootUpdateProgress.BEGIN
|
| 42 |
+
END = RootUpdateProgress.END
|
| 43 |
+
REMOVE = RootUpdateProgress.REMOVE
|
| 44 |
+
BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE
|
| 45 |
+
URLCHANGE = RootUpdateProgress.URLCHANGE
|
| 46 |
+
PATHCHANGE = RootUpdateProgress.PATHCHANGE
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class RootModule(Submodule):
|
| 50 |
+
"""A (virtual) root of all submodules in the given repository.
|
| 51 |
+
|
| 52 |
+
This can be used to more easily traverse all submodules of the
|
| 53 |
+
superproject (master repository).
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
__slots__ = ()
|
| 57 |
+
|
| 58 |
+
k_root_name = "__ROOT__"
|
| 59 |
+
|
| 60 |
+
def __init__(self, repo: "Repo") -> None:
|
| 61 |
+
# repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
|
| 62 |
+
super().__init__(
|
| 63 |
+
repo,
|
| 64 |
+
binsha=self.NULL_BIN_SHA,
|
| 65 |
+
mode=self.k_default_mode,
|
| 66 |
+
path="",
|
| 67 |
+
name=self.k_root_name,
|
| 68 |
+
parent_commit=repo.head.commit,
|
| 69 |
+
url="",
|
| 70 |
+
branch_path=git.Head.to_full_path(self.k_head_default),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def _clear_cache(self) -> None:
|
| 74 |
+
"""May not do anything."""
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
# { Interface
|
| 78 |
+
|
| 79 |
+
def update( # type: ignore[override]
|
| 80 |
+
self,
|
| 81 |
+
previous_commit: Union[Commit_ish, str, None] = None,
|
| 82 |
+
recursive: bool = True,
|
| 83 |
+
force_remove: bool = False,
|
| 84 |
+
init: bool = True,
|
| 85 |
+
to_latest_revision: bool = False,
|
| 86 |
+
progress: Union[None, "RootUpdateProgress"] = None,
|
| 87 |
+
dry_run: bool = False,
|
| 88 |
+
force_reset: bool = False,
|
| 89 |
+
keep_going: bool = False,
|
| 90 |
+
) -> "RootModule":
|
| 91 |
+
"""Update the submodules of this repository to the current HEAD commit.
|
| 92 |
+
|
| 93 |
+
This method behaves smartly by determining changes of the path of a submodule's
|
| 94 |
+
repository, next to changes to the to-be-checked-out commit or the branch to be
|
| 95 |
+
checked out. This works if the submodule's ID does not change.
|
| 96 |
+
|
| 97 |
+
Additionally it will detect addition and removal of submodules, which will be
|
| 98 |
+
handled gracefully.
|
| 99 |
+
|
| 100 |
+
:param previous_commit:
|
| 101 |
+
If set to a commit-ish, the commit we should use as the previous commit the
|
| 102 |
+
HEAD pointed to before it was set to the commit it points to now.
|
| 103 |
+
If ``None``, it defaults to ``HEAD@{1}`` otherwise.
|
| 104 |
+
|
| 105 |
+
:param recursive:
|
| 106 |
+
If ``True``, the children of submodules will be updated as well using the
|
| 107 |
+
same technique.
|
| 108 |
+
|
| 109 |
+
:param force_remove:
|
| 110 |
+
If submodules have been deleted, they will be forcibly removed. Otherwise
|
| 111 |
+
the update may fail if a submodule's repository cannot be deleted as changes
|
| 112 |
+
have been made to it.
|
| 113 |
+
(See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
|
| 114 |
+
for more information.)
|
| 115 |
+
|
| 116 |
+
:param init:
|
| 117 |
+
If we encounter a new module which would need to be initialized, then do it.
|
| 118 |
+
|
| 119 |
+
:param to_latest_revision:
|
| 120 |
+
If ``True``, instead of checking out the revision pointed to by this
|
| 121 |
+
submodule's sha, the checked out tracking branch will be merged with the
|
| 122 |
+
latest remote branch fetched from the repository's origin.
|
| 123 |
+
|
| 124 |
+
Unless `force_reset` is specified, a local tracking branch will never be
|
| 125 |
+
reset into its past, therefore the remote branch must be in the future for
|
| 126 |
+
this to have an effect.
|
| 127 |
+
|
| 128 |
+
:param force_reset:
|
| 129 |
+
If ``True``, submodules may checkout or reset their branch even if the
|
| 130 |
+
repository has pending changes that would be overwritten, or if the local
|
| 131 |
+
tracking branch is in the future of the remote tracking branch and would be
|
| 132 |
+
reset into its past.
|
| 133 |
+
|
| 134 |
+
:param progress:
|
| 135 |
+
:class:`RootUpdateProgress` instance, or ``None`` if no progress should be
|
| 136 |
+
sent.
|
| 137 |
+
|
| 138 |
+
:param dry_run:
|
| 139 |
+
If ``True``, operations will not actually be performed. Progress messages
|
| 140 |
+
will change accordingly to indicate the WOULD DO state of the operation.
|
| 141 |
+
|
| 142 |
+
:param keep_going:
|
| 143 |
+
If ``True``, we will ignore but log all errors, and keep going recursively.
|
| 144 |
+
Unless `dry_run` is set as well, `keep_going` could cause
|
| 145 |
+
subsequent/inherited errors you wouldn't see otherwise.
|
| 146 |
+
In conjunction with `dry_run`, this can be useful to anticipate all errors
|
| 147 |
+
when updating submodules.
|
| 148 |
+
|
| 149 |
+
:return:
|
| 150 |
+
self
|
| 151 |
+
"""
|
| 152 |
+
if self.repo.bare:
|
| 153 |
+
raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
|
| 154 |
+
# END handle bare
|
| 155 |
+
|
| 156 |
+
if progress is None:
|
| 157 |
+
progress = RootUpdateProgress()
|
| 158 |
+
# END ensure progress is set
|
| 159 |
+
|
| 160 |
+
prefix = ""
|
| 161 |
+
if dry_run:
|
| 162 |
+
prefix = "DRY-RUN: "
|
| 163 |
+
|
| 164 |
+
repo = self.repo
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
# SETUP BASE COMMIT
|
| 168 |
+
###################
|
| 169 |
+
cur_commit = repo.head.commit
|
| 170 |
+
if previous_commit is None:
|
| 171 |
+
try:
|
| 172 |
+
previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha)
|
| 173 |
+
if previous_commit.binsha == previous_commit.NULL_BIN_SHA:
|
| 174 |
+
raise IndexError
|
| 175 |
+
# END handle initial commit
|
| 176 |
+
except IndexError:
|
| 177 |
+
# In new repositories, there is no previous commit.
|
| 178 |
+
previous_commit = cur_commit
|
| 179 |
+
# END exception handling
|
| 180 |
+
else:
|
| 181 |
+
previous_commit = repo.commit(previous_commit) # Obtain commit object.
|
| 182 |
+
# END handle previous commit
|
| 183 |
+
|
| 184 |
+
psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
|
| 185 |
+
sms: "IterableList[Submodule]" = self.list_items(repo)
|
| 186 |
+
spsms = set(psms)
|
| 187 |
+
ssms = set(sms)
|
| 188 |
+
|
| 189 |
+
# HANDLE REMOVALS
|
| 190 |
+
###################
|
| 191 |
+
rrsm = spsms - ssms
|
| 192 |
+
len_rrsm = len(rrsm)
|
| 193 |
+
|
| 194 |
+
for i, rsm in enumerate(rrsm):
|
| 195 |
+
op = REMOVE
|
| 196 |
+
if i == 0:
|
| 197 |
+
op |= BEGIN
|
| 198 |
+
# END handle begin
|
| 199 |
+
|
| 200 |
+
# Fake it into thinking its at the current commit to allow deletion
|
| 201 |
+
# of previous module. Trigger the cache to be updated before that.
|
| 202 |
+
progress.update(
|
| 203 |
+
op,
|
| 204 |
+
i,
|
| 205 |
+
len_rrsm,
|
| 206 |
+
prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath),
|
| 207 |
+
)
|
| 208 |
+
rsm._parent_commit = repo.head.commit
|
| 209 |
+
rsm.remove(
|
| 210 |
+
configuration=False,
|
| 211 |
+
module=True,
|
| 212 |
+
force=force_remove,
|
| 213 |
+
dry_run=dry_run,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
if i == len_rrsm - 1:
|
| 217 |
+
op |= END
|
| 218 |
+
# END handle end
|
| 219 |
+
progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name)
|
| 220 |
+
# END for each removed submodule
|
| 221 |
+
|
| 222 |
+
# HANDLE PATH RENAMES
|
| 223 |
+
#####################
|
| 224 |
+
# URL changes + branch changes.
|
| 225 |
+
csms = spsms & ssms
|
| 226 |
+
len_csms = len(csms)
|
| 227 |
+
for i, csm in enumerate(csms):
|
| 228 |
+
psm: "Submodule" = psms[csm.name]
|
| 229 |
+
sm: "Submodule" = sms[csm.name]
|
| 230 |
+
|
| 231 |
+
# PATH CHANGES
|
| 232 |
+
##############
|
| 233 |
+
if sm.path != psm.path and psm.module_exists():
|
| 234 |
+
progress.update(
|
| 235 |
+
BEGIN | PATHCHANGE,
|
| 236 |
+
i,
|
| 237 |
+
len_csms,
|
| 238 |
+
prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath),
|
| 239 |
+
)
|
| 240 |
+
# Move the module to the new path.
|
| 241 |
+
if not dry_run:
|
| 242 |
+
psm.move(sm.path, module=True, configuration=False)
|
| 243 |
+
# END handle dry_run
|
| 244 |
+
progress.update(
|
| 245 |
+
END | PATHCHANGE,
|
| 246 |
+
i,
|
| 247 |
+
len_csms,
|
| 248 |
+
prefix + "Done moving repository of submodule %r" % sm.name,
|
| 249 |
+
)
|
| 250 |
+
# END handle path changes
|
| 251 |
+
|
| 252 |
+
if sm.module_exists():
|
| 253 |
+
# HANDLE URL CHANGE
|
| 254 |
+
###################
|
| 255 |
+
if sm.url != psm.url:
|
| 256 |
+
# Add the new remote, remove the old one.
|
| 257 |
+
# This way, if the url just changes, the commits will not have
|
| 258 |
+
# to be re-retrieved.
|
| 259 |
+
nn = "__new_origin__"
|
| 260 |
+
smm = sm.module()
|
| 261 |
+
rmts = smm.remotes
|
| 262 |
+
|
| 263 |
+
# Don't do anything if we already have the url we search in
|
| 264 |
+
# place.
|
| 265 |
+
if len([r for r in rmts if r.url == sm.url]) == 0:
|
| 266 |
+
progress.update(
|
| 267 |
+
BEGIN | URLCHANGE,
|
| 268 |
+
i,
|
| 269 |
+
len_csms,
|
| 270 |
+
prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url),
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if not dry_run:
|
| 274 |
+
assert nn not in [r.name for r in rmts]
|
| 275 |
+
smr = smm.create_remote(nn, sm.url)
|
| 276 |
+
smr.fetch(progress=progress)
|
| 277 |
+
|
| 278 |
+
# If we have a tracking branch, it should be available
|
| 279 |
+
# in the new remote as well.
|
| 280 |
+
if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
|
| 281 |
+
raise ValueError(
|
| 282 |
+
"Submodule branch named %r was not available in new submodule remote at %r"
|
| 283 |
+
% (sm.branch_name, sm.url)
|
| 284 |
+
)
|
| 285 |
+
# END head is not detached
|
| 286 |
+
|
| 287 |
+
# Now delete the changed one.
|
| 288 |
+
rmt_for_deletion = None
|
| 289 |
+
for remote in rmts:
|
| 290 |
+
if remote.url == psm.url:
|
| 291 |
+
rmt_for_deletion = remote
|
| 292 |
+
break
|
| 293 |
+
# END if urls match
|
| 294 |
+
# END for each remote
|
| 295 |
+
|
| 296 |
+
# If we didn't find a matching remote, but have exactly
|
| 297 |
+
# one, we can safely use this one.
|
| 298 |
+
if rmt_for_deletion is None:
|
| 299 |
+
if len(rmts) == 1:
|
| 300 |
+
rmt_for_deletion = rmts[0]
|
| 301 |
+
else:
|
| 302 |
+
# If we have not found any remote with the
|
| 303 |
+
# original URL we may not have a name. This is a
|
| 304 |
+
# special case, and its okay to fail here.
|
| 305 |
+
# Alternatively we could just generate a unique
|
| 306 |
+
# name and leave all existing ones in place.
|
| 307 |
+
raise InvalidGitRepositoryError(
|
| 308 |
+
"Couldn't find original remote-repo at url %r" % psm.url
|
| 309 |
+
)
|
| 310 |
+
# END handle one single remote
|
| 311 |
+
# END handle check we found a remote
|
| 312 |
+
|
| 313 |
+
orig_name = rmt_for_deletion.name
|
| 314 |
+
smm.delete_remote(rmt_for_deletion)
|
| 315 |
+
# NOTE: Currently we leave tags from the deleted remotes
|
| 316 |
+
# as well as separate tracking branches in the possibly
|
| 317 |
+
# totally changed repository (someone could have changed
|
| 318 |
+
# the url to another project). At some point, one might
|
| 319 |
+
# want to clean it up, but the danger is high to remove
|
| 320 |
+
# stuff the user has added explicitly.
|
| 321 |
+
|
| 322 |
+
# Rename the new remote back to what it was.
|
| 323 |
+
smr.rename(orig_name)
|
| 324 |
+
|
| 325 |
+
# Early on, we verified that the our current tracking
|
| 326 |
+
# branch exists in the remote. Now we have to ensure
|
| 327 |
+
# that the sha we point to is still contained in the new
|
| 328 |
+
# remote tracking branch.
|
| 329 |
+
smsha = sm.binsha
|
| 330 |
+
found = False
|
| 331 |
+
rref = smr.refs[self.branch_name]
|
| 332 |
+
for c in rref.commit.traverse():
|
| 333 |
+
if c.binsha == smsha:
|
| 334 |
+
found = True
|
| 335 |
+
break
|
| 336 |
+
# END traverse all commits in search for sha
|
| 337 |
+
# END for each commit
|
| 338 |
+
|
| 339 |
+
if not found:
|
| 340 |
+
# Adjust our internal binsha to use the one of the
|
| 341 |
+
# remote this way, it will be checked out in the
|
| 342 |
+
# next step. This will change the submodule relative
|
| 343 |
+
# to us, so the user will be able to commit the
|
| 344 |
+
# change easily.
|
| 345 |
+
_logger.warning(
|
| 346 |
+
"Current sha %s was not contained in the tracking\
|
| 347 |
+
branch at the new remote, setting it the the remote's tracking branch",
|
| 348 |
+
sm.hexsha,
|
| 349 |
+
)
|
| 350 |
+
sm.binsha = rref.commit.binsha
|
| 351 |
+
# END reset binsha
|
| 352 |
+
|
| 353 |
+
# NOTE: All checkout is performed by the base
|
| 354 |
+
# implementation of update.
|
| 355 |
+
# END handle dry_run
|
| 356 |
+
progress.update(
|
| 357 |
+
END | URLCHANGE,
|
| 358 |
+
i,
|
| 359 |
+
len_csms,
|
| 360 |
+
prefix + "Done adjusting url of submodule %r" % (sm.name),
|
| 361 |
+
)
|
| 362 |
+
# END skip remote handling if new url already exists in module
|
| 363 |
+
# END handle url
|
| 364 |
+
|
| 365 |
+
# HANDLE PATH CHANGES
|
| 366 |
+
#####################
|
| 367 |
+
if sm.branch_path != psm.branch_path:
|
| 368 |
+
# Finally, create a new tracking branch which tracks the new
|
| 369 |
+
# remote branch.
|
| 370 |
+
progress.update(
|
| 371 |
+
BEGIN | BRANCHCHANGE,
|
| 372 |
+
i,
|
| 373 |
+
len_csms,
|
| 374 |
+
prefix
|
| 375 |
+
+ "Changing branch of submodule %r from %s to %s"
|
| 376 |
+
% (sm.name, psm.branch_path, sm.branch_path),
|
| 377 |
+
)
|
| 378 |
+
if not dry_run:
|
| 379 |
+
smm = sm.module()
|
| 380 |
+
smmr = smm.remotes
|
| 381 |
+
# As the branch might not exist yet, we will have to fetch
|
| 382 |
+
# all remotes to be sure...
|
| 383 |
+
for remote in smmr:
|
| 384 |
+
remote.fetch(progress=progress)
|
| 385 |
+
# END for each remote
|
| 386 |
+
|
| 387 |
+
try:
|
| 388 |
+
tbr = git.Head.create(
|
| 389 |
+
smm,
|
| 390 |
+
sm.branch_name,
|
| 391 |
+
logmsg="branch: Created from HEAD",
|
| 392 |
+
)
|
| 393 |
+
except OSError:
|
| 394 |
+
# ...or reuse the existing one.
|
| 395 |
+
tbr = git.Head(smm, sm.branch_path)
|
| 396 |
+
# END ensure tracking branch exists
|
| 397 |
+
|
| 398 |
+
tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
|
| 399 |
+
# NOTE: All head-resetting is done in the base
|
| 400 |
+
# implementation of update but we will have to checkout the
|
| 401 |
+
# new branch here. As it still points to the currently
|
| 402 |
+
# checked out commit, we don't do any harm.
|
| 403 |
+
# As we don't want to update working-tree or index, changing
|
| 404 |
+
# the ref is all there is to do.
|
| 405 |
+
smm.head.reference = tbr
|
| 406 |
+
# END handle dry_run
|
| 407 |
+
|
| 408 |
+
progress.update(
|
| 409 |
+
END | BRANCHCHANGE,
|
| 410 |
+
i,
|
| 411 |
+
len_csms,
|
| 412 |
+
prefix + "Done changing branch of submodule %r" % sm.name,
|
| 413 |
+
)
|
| 414 |
+
# END handle branch
|
| 415 |
+
# END handle
|
| 416 |
+
# END for each common submodule
|
| 417 |
+
except Exception as err:
|
| 418 |
+
if not keep_going:
|
| 419 |
+
raise
|
| 420 |
+
_logger.error(str(err))
|
| 421 |
+
# END handle keep_going
|
| 422 |
+
|
| 423 |
+
# FINALLY UPDATE ALL ACTUAL SUBMODULES
|
| 424 |
+
######################################
|
| 425 |
+
for sm in sms:
|
| 426 |
+
# Update the submodule using the default method.
|
| 427 |
+
sm.update(
|
| 428 |
+
recursive=False,
|
| 429 |
+
init=init,
|
| 430 |
+
to_latest_revision=to_latest_revision,
|
| 431 |
+
progress=progress,
|
| 432 |
+
dry_run=dry_run,
|
| 433 |
+
force=force_reset,
|
| 434 |
+
keep_going=keep_going,
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
# Update recursively depth first - question is which inconsistent state will
|
| 438 |
+
# be better in case it fails somewhere. Defective branch or defective depth.
|
| 439 |
+
# The RootSubmodule type will never process itself, which was done in the
|
| 440 |
+
# previous expression.
|
| 441 |
+
if recursive:
|
| 442 |
+
# The module would exist by now if we are not in dry_run mode.
|
| 443 |
+
if sm.module_exists():
|
| 444 |
+
type(self)(sm.module()).update(
|
| 445 |
+
recursive=True,
|
| 446 |
+
force_remove=force_remove,
|
| 447 |
+
init=init,
|
| 448 |
+
to_latest_revision=to_latest_revision,
|
| 449 |
+
progress=progress,
|
| 450 |
+
dry_run=dry_run,
|
| 451 |
+
force_reset=force_reset,
|
| 452 |
+
keep_going=keep_going,
|
| 453 |
+
)
|
| 454 |
+
# END handle dry_run
|
| 455 |
+
# END handle recursive
|
| 456 |
+
# END for each submodule to update
|
| 457 |
+
|
| 458 |
+
return self
|
| 459 |
+
|
| 460 |
+
def module(self) -> "Repo":
|
| 461 |
+
""":return: The actual repository containing the submodules"""
|
| 462 |
+
return self.repo
|
| 463 |
+
|
| 464 |
+
# } END interface
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
# } END classes
|
venv/lib/python3.10/site-packages/git/objects/submodule/util.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is part of GitPython and is released under the
|
| 2 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
"sm_section",
|
| 6 |
+
"sm_name",
|
| 7 |
+
"mkhead",
|
| 8 |
+
"find_first_remote_branch",
|
| 9 |
+
"SubmoduleConfigParser",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
from io import BytesIO
|
| 13 |
+
import weakref
|
| 14 |
+
|
| 15 |
+
import git
|
| 16 |
+
from git.config import GitConfigParser
|
| 17 |
+
from git.exc import InvalidGitRepositoryError
|
| 18 |
+
|
| 19 |
+
# typing -----------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
from typing import Any, Sequence, TYPE_CHECKING, Union
|
| 22 |
+
|
| 23 |
+
from git.types import PathLike
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from weakref import ReferenceType
|
| 27 |
+
|
| 28 |
+
from git.refs import Head, RemoteReference
|
| 29 |
+
from git.remote import Remote
|
| 30 |
+
from git.repo import Repo
|
| 31 |
+
|
| 32 |
+
from .base import Submodule
|
| 33 |
+
|
| 34 |
+
# { Utilities
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def sm_section(name: str) -> str:
|
| 38 |
+
""":return: Section title used in ``.gitmodules`` configuration file"""
|
| 39 |
+
return f'submodule "{name}"'
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def sm_name(section: str) -> str:
|
| 43 |
+
""":return: Name of the submodule as parsed from the section name"""
|
| 44 |
+
section = section.strip()
|
| 45 |
+
return section[11:-1]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def mkhead(repo: "Repo", path: PathLike) -> "Head":
|
| 49 |
+
""":return: New branch/head instance"""
|
| 50 |
+
return git.Head(repo, git.Head.to_full_path(path))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
|
| 54 |
+
"""Find the remote branch matching the name of the given branch or raise
|
| 55 |
+
:exc:`~git.exc.InvalidGitRepositoryError`."""
|
| 56 |
+
for remote in remotes:
|
| 57 |
+
try:
|
| 58 |
+
return remote.refs[branch_name]
|
| 59 |
+
except IndexError:
|
| 60 |
+
continue
|
| 61 |
+
# END exception handling
|
| 62 |
+
# END for remote
|
| 63 |
+
raise InvalidGitRepositoryError("Didn't find remote branch '%r' in any of the given remotes" % branch_name)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# } END utilities
|
| 67 |
+
|
| 68 |
+
# { Classes
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class SubmoduleConfigParser(GitConfigParser):
|
| 72 |
+
"""Catches calls to :meth:`~git.config.GitConfigParser.write`, and updates the
|
| 73 |
+
``.gitmodules`` blob in the index with the new data, if we have written into a
|
| 74 |
+
stream.
|
| 75 |
+
|
| 76 |
+
Otherwise it would add the local file to the index to make it correspond with the
|
| 77 |
+
working tree. Additionally, the cache must be cleared.
|
| 78 |
+
|
| 79 |
+
Please note that no mutating method will work in bare mode.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
| 83 |
+
self._smref: Union["ReferenceType[Submodule]", None] = None
|
| 84 |
+
self._index = None
|
| 85 |
+
self._auto_write = True
|
| 86 |
+
super().__init__(*args, **kwargs)
|
| 87 |
+
|
| 88 |
+
# { Interface
|
| 89 |
+
def set_submodule(self, submodule: "Submodule") -> None:
|
| 90 |
+
"""Set this instance's submodule. It must be called before the first write
|
| 91 |
+
operation begins."""
|
| 92 |
+
self._smref = weakref.ref(submodule)
|
| 93 |
+
|
| 94 |
+
def flush_to_index(self) -> None:
|
| 95 |
+
"""Flush changes in our configuration file to the index."""
|
| 96 |
+
assert self._smref is not None
|
| 97 |
+
# Should always have a file here.
|
| 98 |
+
assert not isinstance(self._file_or_files, BytesIO)
|
| 99 |
+
|
| 100 |
+
sm = self._smref()
|
| 101 |
+
if sm is not None:
|
| 102 |
+
index = self._index
|
| 103 |
+
if index is None:
|
| 104 |
+
index = sm.repo.index
|
| 105 |
+
# END handle index
|
| 106 |
+
index.add([sm.k_modules_file], write=self._auto_write)
|
| 107 |
+
sm._clear_cache()
|
| 108 |
+
# END handle weakref
|
| 109 |
+
|
| 110 |
+
# } END interface
|
| 111 |
+
|
| 112 |
+
# { Overridden Methods
|
| 113 |
+
def write(self) -> None: # type: ignore[override]
|
| 114 |
+
rval: None = super().write()
|
| 115 |
+
self.flush_to_index()
|
| 116 |
+
return rval
|
| 117 |
+
|
| 118 |
+
# END overridden methods
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# } END classes
|
venv/lib/python3.10/site-packages/git/objects/tag.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
"""Provides an :class:`~git.objects.base.Object`-based type for annotated tags.
|
| 7 |
+
|
| 8 |
+
This defines the :class:`TagObject` class, which represents annotated tags.
|
| 9 |
+
For lightweight tags, see the :mod:`git.refs.tag` module.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
__all__ = ["TagObject"]
|
| 13 |
+
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
from git.compat import defenc
|
| 17 |
+
from git.util import Actor, hex_to_bin
|
| 18 |
+
|
| 19 |
+
from . import base
|
| 20 |
+
from .util import get_object_type_by_name, parse_actor_and_date
|
| 21 |
+
|
| 22 |
+
# typing ----------------------------------------------
|
| 23 |
+
|
| 24 |
+
from typing import List, TYPE_CHECKING, Union
|
| 25 |
+
|
| 26 |
+
if sys.version_info >= (3, 8):
|
| 27 |
+
from typing import Literal
|
| 28 |
+
else:
|
| 29 |
+
from typing_extensions import Literal
|
| 30 |
+
|
| 31 |
+
if TYPE_CHECKING:
|
| 32 |
+
from git.repo import Repo
|
| 33 |
+
|
| 34 |
+
from .blob import Blob
|
| 35 |
+
from .commit import Commit
|
| 36 |
+
from .tree import Tree
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class TagObject(base.Object):
|
| 42 |
+
"""Annotated (i.e. non-lightweight) tag carrying additional information about an
|
| 43 |
+
object we are pointing to.
|
| 44 |
+
|
| 45 |
+
See :manpage:`gitglossary(7)` on "tag object":
|
| 46 |
+
https://git-scm.com/docs/gitglossary#def_tag_object
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
type: Literal["tag"] = "tag"
|
| 50 |
+
|
| 51 |
+
__slots__ = (
|
| 52 |
+
"object",
|
| 53 |
+
"tag",
|
| 54 |
+
"tagger",
|
| 55 |
+
"tagged_date",
|
| 56 |
+
"tagger_tz_offset",
|
| 57 |
+
"message",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self,
|
| 62 |
+
repo: "Repo",
|
| 63 |
+
binsha: bytes,
|
| 64 |
+
object: Union[None, base.Object] = None,
|
| 65 |
+
tag: Union[None, str] = None,
|
| 66 |
+
tagger: Union[None, Actor] = None,
|
| 67 |
+
tagged_date: Union[int, None] = None,
|
| 68 |
+
tagger_tz_offset: Union[int, None] = None,
|
| 69 |
+
message: Union[str, None] = None,
|
| 70 |
+
) -> None: # @ReservedAssignment
|
| 71 |
+
"""Initialize a tag object with additional data.
|
| 72 |
+
|
| 73 |
+
:param repo:
|
| 74 |
+
Repository this object is located in.
|
| 75 |
+
|
| 76 |
+
:param binsha:
|
| 77 |
+
20 byte SHA1.
|
| 78 |
+
|
| 79 |
+
:param object:
|
| 80 |
+
:class:`~git.objects.base.Object` instance of object we are pointing to.
|
| 81 |
+
|
| 82 |
+
:param tag:
|
| 83 |
+
Name of this tag.
|
| 84 |
+
|
| 85 |
+
:param tagger:
|
| 86 |
+
:class:`~git.util.Actor` identifying the tagger.
|
| 87 |
+
|
| 88 |
+
:param tagged_date: int_seconds_since_epoch
|
| 89 |
+
The DateTime of the tag creation.
|
| 90 |
+
Use :func:`time.gmtime` to convert it into a different format.
|
| 91 |
+
|
| 92 |
+
:param tagger_tz_offset: int_seconds_west_of_utc
|
| 93 |
+
The timezone that the `tagged_date` is in, in a format similar to
|
| 94 |
+
:attr:`time.altzone`.
|
| 95 |
+
"""
|
| 96 |
+
super().__init__(repo, binsha)
|
| 97 |
+
if object is not None:
|
| 98 |
+
self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object
|
| 99 |
+
if tag is not None:
|
| 100 |
+
self.tag = tag
|
| 101 |
+
if tagger is not None:
|
| 102 |
+
self.tagger = tagger
|
| 103 |
+
if tagged_date is not None:
|
| 104 |
+
self.tagged_date = tagged_date
|
| 105 |
+
if tagger_tz_offset is not None:
|
| 106 |
+
self.tagger_tz_offset = tagger_tz_offset
|
| 107 |
+
if message is not None:
|
| 108 |
+
self.message = message
|
| 109 |
+
|
| 110 |
+
def _set_cache_(self, attr: str) -> None:
|
| 111 |
+
"""Cache all our attributes at once."""
|
| 112 |
+
if attr in TagObject.__slots__:
|
| 113 |
+
ostream = self.repo.odb.stream(self.binsha)
|
| 114 |
+
lines: List[str] = ostream.read().decode(defenc, "replace").splitlines()
|
| 115 |
+
|
| 116 |
+
_obj, hexsha = lines[0].split(" ")
|
| 117 |
+
_type_token, type_name = lines[1].split(" ")
|
| 118 |
+
object_type = get_object_type_by_name(type_name.encode("ascii"))
|
| 119 |
+
self.object = object_type(self.repo, hex_to_bin(hexsha))
|
| 120 |
+
|
| 121 |
+
self.tag = lines[2][4:] # tag <tag name>
|
| 122 |
+
|
| 123 |
+
if len(lines) > 3:
|
| 124 |
+
tagger_info = lines[3] # tagger <actor> <date>
|
| 125 |
+
(
|
| 126 |
+
self.tagger,
|
| 127 |
+
self.tagged_date,
|
| 128 |
+
self.tagger_tz_offset,
|
| 129 |
+
) = parse_actor_and_date(tagger_info)
|
| 130 |
+
|
| 131 |
+
# Line 4 empty - it could mark the beginning of the next header.
|
| 132 |
+
# In case there really is no message, it would not exist.
|
| 133 |
+
# Otherwise a newline separates header from message.
|
| 134 |
+
if len(lines) > 5:
|
| 135 |
+
self.message = "\n".join(lines[5:])
|
| 136 |
+
else:
|
| 137 |
+
self.message = ""
|
| 138 |
+
# END check our attributes
|
| 139 |
+
else:
|
| 140 |
+
super()._set_cache_(attr)
|
venv/lib/python3.10/site-packages/git/objects/tree.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
__all__ = ["TreeModifier", "Tree"]
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
import git.diff as git_diff
|
| 11 |
+
from git.util import IterableList, join_path, to_bin_sha
|
| 12 |
+
|
| 13 |
+
from . import util
|
| 14 |
+
from .base import IndexObjUnion, IndexObject
|
| 15 |
+
from .blob import Blob
|
| 16 |
+
from .fun import tree_entries_from_data, tree_to_stream
|
| 17 |
+
from .submodule.base import Submodule
|
| 18 |
+
|
| 19 |
+
# typing -------------------------------------------------
|
| 20 |
+
|
| 21 |
+
from typing import (
|
| 22 |
+
Any,
|
| 23 |
+
Callable,
|
| 24 |
+
Dict,
|
| 25 |
+
Iterable,
|
| 26 |
+
Iterator,
|
| 27 |
+
List,
|
| 28 |
+
Tuple,
|
| 29 |
+
TYPE_CHECKING,
|
| 30 |
+
Type,
|
| 31 |
+
Union,
|
| 32 |
+
cast,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if sys.version_info >= (3, 8):
|
| 36 |
+
from typing import Literal
|
| 37 |
+
else:
|
| 38 |
+
from typing_extensions import Literal
|
| 39 |
+
|
| 40 |
+
from git.types import PathLike
|
| 41 |
+
|
| 42 |
+
if TYPE_CHECKING:
|
| 43 |
+
from io import BytesIO
|
| 44 |
+
|
| 45 |
+
from git.repo import Repo
|
| 46 |
+
|
| 47 |
+
TreeCacheTup = Tuple[bytes, int, str]
|
| 48 |
+
|
| 49 |
+
TraversedTreeTup = Union[Tuple[Union["Tree", None], IndexObjUnion, Tuple["Submodule", "Submodule"]]]
|
| 50 |
+
|
| 51 |
+
# --------------------------------------------------------
|
| 52 |
+
|
| 53 |
+
cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class TreeModifier:
|
| 57 |
+
"""A utility class providing methods to alter the underlying cache in a list-like
|
| 58 |
+
fashion.
|
| 59 |
+
|
| 60 |
+
Once all adjustments are complete, the :attr:`_cache`, which really is a reference
|
| 61 |
+
to the cache of a tree, will be sorted. This ensures it will be in a serializable
|
| 62 |
+
state.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
__slots__ = ("_cache",)
|
| 66 |
+
|
| 67 |
+
def __init__(self, cache: List[TreeCacheTup]) -> None:
|
| 68 |
+
self._cache = cache
|
| 69 |
+
|
| 70 |
+
def _index_by_name(self, name: str) -> int:
|
| 71 |
+
""":return: index of an item with name, or -1 if not found"""
|
| 72 |
+
for i, t in enumerate(self._cache):
|
| 73 |
+
if t[2] == name:
|
| 74 |
+
return i
|
| 75 |
+
# END found item
|
| 76 |
+
# END for each item in cache
|
| 77 |
+
return -1
|
| 78 |
+
|
| 79 |
+
# { Interface
|
| 80 |
+
def set_done(self) -> "TreeModifier":
|
| 81 |
+
"""Call this method once you are done modifying the tree information.
|
| 82 |
+
|
| 83 |
+
This may be called several times, but be aware that each call will cause a sort
|
| 84 |
+
operation.
|
| 85 |
+
|
| 86 |
+
:return:
|
| 87 |
+
self
|
| 88 |
+
"""
|
| 89 |
+
self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
|
| 90 |
+
return self
|
| 91 |
+
|
| 92 |
+
# } END interface
|
| 93 |
+
|
| 94 |
+
# { Mutators
|
| 95 |
+
def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeModifier":
|
| 96 |
+
"""Add the given item to the tree.
|
| 97 |
+
|
| 98 |
+
If an item with the given name already exists, nothing will be done, but a
|
| 99 |
+
:exc:`ValueError` will be raised if the sha and mode of the existing item do not
|
| 100 |
+
match the one you add, unless `force` is ``True``.
|
| 101 |
+
|
| 102 |
+
:param sha:
|
| 103 |
+
The 20 or 40 byte sha of the item to add.
|
| 104 |
+
|
| 105 |
+
:param mode:
|
| 106 |
+
:class:`int` representing the stat-compatible mode of the item.
|
| 107 |
+
|
| 108 |
+
:param force:
|
| 109 |
+
If ``True``, an item with your name and information will overwrite any
|
| 110 |
+
existing item with the same name, no matter which information it has.
|
| 111 |
+
|
| 112 |
+
:return:
|
| 113 |
+
self
|
| 114 |
+
"""
|
| 115 |
+
if "/" in name:
|
| 116 |
+
raise ValueError("Name must not contain '/' characters")
|
| 117 |
+
if (mode >> 12) not in Tree._map_id_to_type:
|
| 118 |
+
raise ValueError("Invalid object type according to mode %o" % mode)
|
| 119 |
+
|
| 120 |
+
sha = to_bin_sha(sha)
|
| 121 |
+
index = self._index_by_name(name)
|
| 122 |
+
|
| 123 |
+
item = (sha, mode, name)
|
| 124 |
+
|
| 125 |
+
if index == -1:
|
| 126 |
+
self._cache.append(item)
|
| 127 |
+
else:
|
| 128 |
+
if force:
|
| 129 |
+
self._cache[index] = item
|
| 130 |
+
else:
|
| 131 |
+
ex_item = self._cache[index]
|
| 132 |
+
if ex_item[0] != sha or ex_item[1] != mode:
|
| 133 |
+
raise ValueError("Item %r existed with different properties" % name)
|
| 134 |
+
# END handle mismatch
|
| 135 |
+
# END handle force
|
| 136 |
+
# END handle name exists
|
| 137 |
+
return self
|
| 138 |
+
|
| 139 |
+
def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
|
| 140 |
+
"""Add the given item to the tree. Its correctness is assumed, so it is the
|
| 141 |
+
caller's responsibility to ensure that the input is correct.
|
| 142 |
+
|
| 143 |
+
For more information on the parameters, see :meth:`add`.
|
| 144 |
+
|
| 145 |
+
:param binsha:
|
| 146 |
+
20 byte binary sha.
|
| 147 |
+
"""
|
| 148 |
+
assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str)
|
| 149 |
+
tree_cache = (binsha, mode, name)
|
| 150 |
+
|
| 151 |
+
self._cache.append(tree_cache)
|
| 152 |
+
|
| 153 |
+
def __delitem__(self, name: str) -> None:
|
| 154 |
+
"""Delete an item with the given name if it exists."""
|
| 155 |
+
index = self._index_by_name(name)
|
| 156 |
+
if index > -1:
|
| 157 |
+
del self._cache[index]
|
| 158 |
+
|
| 159 |
+
# } END mutators
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
|
| 163 |
+
R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
|
| 164 |
+
other :class:`Tree`\s.
|
| 165 |
+
|
| 166 |
+
See :manpage:`gitglossary(7)` on "tree object":
|
| 167 |
+
https://git-scm.com/docs/gitglossary#def_tree_object
|
| 168 |
+
|
| 169 |
+
Subscripting is supported, as with a list or dict:
|
| 170 |
+
|
| 171 |
+
* Access a specific blob using the ``tree["filename"]`` notation.
|
| 172 |
+
* You may likewise access by index, like ``blob = tree[0]``.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
type: Literal["tree"] = "tree"
|
| 176 |
+
|
| 177 |
+
__slots__ = ("_cache",)
|
| 178 |
+
|
| 179 |
+
# Actual integer IDs for comparison.
|
| 180 |
+
commit_id = 0o16 # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link.
|
| 181 |
+
blob_id = 0o10
|
| 182 |
+
symlink_id = 0o12
|
| 183 |
+
tree_id = 0o04
|
| 184 |
+
|
| 185 |
+
_map_id_to_type: Dict[int, Type[IndexObjUnion]] = {
|
| 186 |
+
commit_id: Submodule,
|
| 187 |
+
blob_id: Blob,
|
| 188 |
+
symlink_id: Blob,
|
| 189 |
+
# Tree ID added once Tree is defined.
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
def __init__(
|
| 193 |
+
self,
|
| 194 |
+
repo: "Repo",
|
| 195 |
+
binsha: bytes,
|
| 196 |
+
mode: int = tree_id << 12,
|
| 197 |
+
path: Union[PathLike, None] = None,
|
| 198 |
+
):
|
| 199 |
+
super().__init__(repo, binsha, mode, path)
|
| 200 |
+
|
| 201 |
+
@classmethod
|
| 202 |
+
def _get_intermediate_items(
|
| 203 |
+
cls,
|
| 204 |
+
index_object: IndexObjUnion,
|
| 205 |
+
) -> Union[Tuple["Tree", ...], Tuple[()]]:
|
| 206 |
+
if index_object.type == "tree":
|
| 207 |
+
return tuple(index_object._iter_convert_to_object(index_object._cache))
|
| 208 |
+
return ()
|
| 209 |
+
|
| 210 |
+
def _set_cache_(self, attr: str) -> None:
|
| 211 |
+
if attr == "_cache":
|
| 212 |
+
# Set the data when we need it.
|
| 213 |
+
ostream = self.repo.odb.stream(self.binsha)
|
| 214 |
+
self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read())
|
| 215 |
+
else:
|
| 216 |
+
super()._set_cache_(attr)
|
| 217 |
+
# END handle attribute
|
| 218 |
+
|
| 219 |
+
def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
|
| 220 |
+
"""Iterable yields tuples of (binsha, mode, name), which will be converted to
|
| 221 |
+
the respective object representation.
|
| 222 |
+
"""
|
| 223 |
+
for binsha, mode, name in iterable:
|
| 224 |
+
path = join_path(self.path, name)
|
| 225 |
+
try:
|
| 226 |
+
yield self._map_id_to_type[mode >> 12](self.repo, binsha, mode, path)
|
| 227 |
+
except KeyError as e:
|
| 228 |
+
raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e
|
| 229 |
+
# END for each item
|
| 230 |
+
|
| 231 |
+
def join(self, file: str) -> IndexObjUnion:
|
| 232 |
+
"""Find the named object in this tree's contents.
|
| 233 |
+
|
| 234 |
+
:return:
|
| 235 |
+
:class:`~git.objects.blob.Blob`, :class:`Tree`, or
|
| 236 |
+
:class:`~git.objects.submodule.base.Submodule`
|
| 237 |
+
|
| 238 |
+
:raise KeyError:
|
| 239 |
+
If the given file or tree does not exist in this tree.
|
| 240 |
+
"""
|
| 241 |
+
msg = "Blob or Tree named %r not found"
|
| 242 |
+
if "/" in file:
|
| 243 |
+
tree = self
|
| 244 |
+
item = self
|
| 245 |
+
tokens = file.split("/")
|
| 246 |
+
for i, token in enumerate(tokens):
|
| 247 |
+
item = tree[token]
|
| 248 |
+
if item.type == "tree":
|
| 249 |
+
tree = item
|
| 250 |
+
else:
|
| 251 |
+
# Safety assertion - blobs are at the end of the path.
|
| 252 |
+
if i != len(tokens) - 1:
|
| 253 |
+
raise KeyError(msg % file)
|
| 254 |
+
return item
|
| 255 |
+
# END handle item type
|
| 256 |
+
# END for each token of split path
|
| 257 |
+
if item == self:
|
| 258 |
+
raise KeyError(msg % file)
|
| 259 |
+
return item
|
| 260 |
+
else:
|
| 261 |
+
for info in self._cache:
|
| 262 |
+
if info[2] == file: # [2] == name
|
| 263 |
+
return self._map_id_to_type[info[1] >> 12](
|
| 264 |
+
self.repo, info[0], info[1], join_path(self.path, info[2])
|
| 265 |
+
)
|
| 266 |
+
# END for each obj
|
| 267 |
+
raise KeyError(msg % file)
|
| 268 |
+
# END handle long paths
|
| 269 |
+
|
| 270 |
+
def __truediv__(self, file: str) -> IndexObjUnion:
|
| 271 |
+
"""The ``/`` operator is another syntax for joining.
|
| 272 |
+
|
| 273 |
+
See :meth:`join` for details.
|
| 274 |
+
"""
|
| 275 |
+
return self.join(file)
|
| 276 |
+
|
| 277 |
+
@property
|
| 278 |
+
def trees(self) -> List["Tree"]:
|
| 279 |
+
""":return: list(Tree, ...) List of trees directly below this tree"""
|
| 280 |
+
return [i for i in self if i.type == "tree"]
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def blobs(self) -> List[Blob]:
|
| 284 |
+
""":return: list(Blob, ...) List of blobs directly below this tree"""
|
| 285 |
+
return [i for i in self if i.type == "blob"]
|
| 286 |
+
|
| 287 |
+
@property
|
| 288 |
+
def cache(self) -> TreeModifier:
|
| 289 |
+
"""
|
| 290 |
+
:return:
|
| 291 |
+
An object allowing modification of the internal cache. This can be used to
|
| 292 |
+
change the tree's contents. When done, make sure you call
|
| 293 |
+
:meth:`~TreeModifier.set_done` on the tree modifier, or serialization
|
| 294 |
+
behaviour will be incorrect.
|
| 295 |
+
|
| 296 |
+
:note:
|
| 297 |
+
See :class:`TreeModifier` for more information on how to alter the cache.
|
| 298 |
+
"""
|
| 299 |
+
return TreeModifier(self._cache)
|
| 300 |
+
|
| 301 |
+
def traverse(
|
| 302 |
+
self,
|
| 303 |
+
predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True,
|
| 304 |
+
prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False,
|
| 305 |
+
depth: int = -1,
|
| 306 |
+
branch_first: bool = True,
|
| 307 |
+
visit_once: bool = False,
|
| 308 |
+
ignore_self: int = 1,
|
| 309 |
+
as_edge: bool = False,
|
| 310 |
+
) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]:
|
| 311 |
+
"""For documentation, see
|
| 312 |
+
`Traversable._traverse() <git.objects.util.Traversable._traverse>`.
|
| 313 |
+
|
| 314 |
+
Trees are set to ``visit_once = False`` to gain more performance in the
|
| 315 |
+
traversal.
|
| 316 |
+
"""
|
| 317 |
+
|
| 318 |
+
# # To typecheck instead of using cast.
|
| 319 |
+
# import itertools
|
| 320 |
+
# def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]:
|
| 321 |
+
# return all(isinstance(x, (Blob, Tree, Submodule)) for x in inp[1])
|
| 322 |
+
|
| 323 |
+
# ret = super().traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
|
| 324 |
+
# ret_tup = itertools.tee(ret, 2)
|
| 325 |
+
# assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}"
|
| 326 |
+
# return ret_tup[0]
|
| 327 |
+
|
| 328 |
+
return cast(
|
| 329 |
+
Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
|
| 330 |
+
super()._traverse(
|
| 331 |
+
predicate, # type: ignore[arg-type]
|
| 332 |
+
prune, # type: ignore[arg-type]
|
| 333 |
+
depth,
|
| 334 |
+
branch_first,
|
| 335 |
+
visit_once,
|
| 336 |
+
ignore_self,
|
| 337 |
+
),
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion]:
|
| 341 |
+
"""
|
| 342 |
+
:return:
|
| 343 |
+
:class:`~git.util.IterableList` with the results of the traversal as
|
| 344 |
+
produced by :meth:`traverse`
|
| 345 |
+
|
| 346 |
+
Tree -> IterableList[Union[Submodule, Tree, Blob]]
|
| 347 |
+
"""
|
| 348 |
+
return super()._list_traverse(*args, **kwargs)
|
| 349 |
+
|
| 350 |
+
# List protocol
|
| 351 |
+
|
| 352 |
+
def __getslice__(self, i: int, j: int) -> List[IndexObjUnion]:
|
| 353 |
+
return list(self._iter_convert_to_object(self._cache[i:j]))
|
| 354 |
+
|
| 355 |
+
def __iter__(self) -> Iterator[IndexObjUnion]:
|
| 356 |
+
return self._iter_convert_to_object(self._cache)
|
| 357 |
+
|
| 358 |
+
def __len__(self) -> int:
|
| 359 |
+
return len(self._cache)
|
| 360 |
+
|
| 361 |
+
def __getitem__(self, item: Union[str, int, slice]) -> IndexObjUnion:
|
| 362 |
+
if isinstance(item, int):
|
| 363 |
+
info = self._cache[item]
|
| 364 |
+
return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2]))
|
| 365 |
+
|
| 366 |
+
if isinstance(item, str):
|
| 367 |
+
# compatibility
|
| 368 |
+
return self.join(item)
|
| 369 |
+
# END index is basestring
|
| 370 |
+
|
| 371 |
+
raise TypeError("Invalid index type: %r" % item)
|
| 372 |
+
|
| 373 |
+
def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
|
| 374 |
+
if isinstance(item, IndexObject):
|
| 375 |
+
for info in self._cache:
|
| 376 |
+
if item.binsha == info[0]:
|
| 377 |
+
return True
|
| 378 |
+
# END compare sha
|
| 379 |
+
# END for each entry
|
| 380 |
+
# END handle item is index object
|
| 381 |
+
# compatibility
|
| 382 |
+
|
| 383 |
+
# Treat item as repo-relative path.
|
| 384 |
+
else:
|
| 385 |
+
path = self.path
|
| 386 |
+
for info in self._cache:
|
| 387 |
+
if item == join_path(path, info[2]):
|
| 388 |
+
return True
|
| 389 |
+
# END for each item
|
| 390 |
+
return False
|
| 391 |
+
|
| 392 |
+
def __reversed__(self) -> Iterator[IndexObjUnion]:
|
| 393 |
+
return reversed(self._iter_convert_to_object(self._cache)) # type: ignore[call-overload]
|
| 394 |
+
|
| 395 |
+
def _serialize(self, stream: "BytesIO") -> "Tree":
|
| 396 |
+
"""Serialize this tree into the stream. Assumes sorted tree data.
|
| 397 |
+
|
| 398 |
+
:note:
|
| 399 |
+
We will assume our tree data to be in a sorted state. If this is not the
|
| 400 |
+
case, serialization will not generate a correct tree representation as these
|
| 401 |
+
are assumed to be sorted by algorithms.
|
| 402 |
+
"""
|
| 403 |
+
tree_to_stream(self._cache, stream.write)
|
| 404 |
+
return self
|
| 405 |
+
|
| 406 |
+
def _deserialize(self, stream: "BytesIO") -> "Tree":
|
| 407 |
+
self._cache = tree_entries_from_data(stream.read())
|
| 408 |
+
return self
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
# END tree
|
| 412 |
+
|
| 413 |
+
# Finalize map definition.
|
| 414 |
+
Tree._map_id_to_type[Tree.tree_id] = Tree
|
venv/lib/python3.10/site-packages/git/objects/util.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
|
| 2 |
+
#
|
| 3 |
+
# This module is part of GitPython and is released under the
|
| 4 |
+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
| 5 |
+
|
| 6 |
+
"""Utility functions for working with git objects."""
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"get_object_type_by_name",
|
| 10 |
+
"parse_date",
|
| 11 |
+
"parse_actor_and_date",
|
| 12 |
+
"ProcessStreamAdapter",
|
| 13 |
+
"Traversable",
|
| 14 |
+
"altz_to_utctz_str",
|
| 15 |
+
"utctz_to_altz",
|
| 16 |
+
"verify_utctz",
|
| 17 |
+
"Actor",
|
| 18 |
+
"tzoffset",
|
| 19 |
+
"utc",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
from abc import ABC, abstractmethod
|
| 23 |
+
import calendar
|
| 24 |
+
from collections import deque
|
| 25 |
+
from datetime import datetime, timedelta, tzinfo
|
| 26 |
+
import re
|
| 27 |
+
from string import digits
|
| 28 |
+
import time
|
| 29 |
+
import warnings
|
| 30 |
+
|
| 31 |
+
from git.util import Actor, IterableList, IterableObj
|
| 32 |
+
|
| 33 |
+
# typing ------------------------------------------------------------
|
| 34 |
+
|
| 35 |
+
from typing import (
|
| 36 |
+
Any,
|
| 37 |
+
Callable,
|
| 38 |
+
Deque,
|
| 39 |
+
Iterator,
|
| 40 |
+
NamedTuple,
|
| 41 |
+
Sequence,
|
| 42 |
+
TYPE_CHECKING,
|
| 43 |
+
Tuple,
|
| 44 |
+
Type,
|
| 45 |
+
TypeVar,
|
| 46 |
+
Union,
|
| 47 |
+
cast,
|
| 48 |
+
overload,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
from git.types import Has_id_attribute, Literal
|
| 52 |
+
|
| 53 |
+
if TYPE_CHECKING:
|
| 54 |
+
from io import BytesIO, StringIO
|
| 55 |
+
from subprocess import Popen
|
| 56 |
+
|
| 57 |
+
from git.types import Protocol, runtime_checkable
|
| 58 |
+
|
| 59 |
+
from .blob import Blob
|
| 60 |
+
from .commit import Commit
|
| 61 |
+
from .submodule.base import Submodule
|
| 62 |
+
from .tag import TagObject
|
| 63 |
+
from .tree import TraversedTreeTup, Tree
|
| 64 |
+
else:
|
| 65 |
+
Protocol = ABC
|
| 66 |
+
|
| 67 |
+
def runtime_checkable(f):
|
| 68 |
+
return f
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class TraverseNT(NamedTuple):
|
| 72 |
+
depth: int
|
| 73 |
+
item: Union["Traversable", "Blob"]
|
| 74 |
+
src: Union["Traversable", None]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj") # For TraversableIterableObj.traverse()
|
| 78 |
+
|
| 79 |
+
TraversedTup = Union[
|
| 80 |
+
Tuple[Union["Traversable", None], "Traversable"], # For Commit, Submodule.
|
| 81 |
+
"TraversedTreeTup", # For Tree.traverse().
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
# --------------------------------------------------------------------
|
| 85 |
+
|
| 86 |
+
ZERO = timedelta(0)
|
| 87 |
+
|
| 88 |
+
# { Functions
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def mode_str_to_int(modestr: Union[bytes, str]) -> int:
|
| 92 |
+
"""Convert mode bits from an octal mode string to an integer mode for git.
|
| 93 |
+
|
| 94 |
+
:param modestr:
|
| 95 |
+
String like ``755`` or ``644`` or ``100644`` - only the last 6 chars will be
|
| 96 |
+
used.
|
| 97 |
+
|
| 98 |
+
:return:
|
| 99 |
+
String identifying a mode compatible to the mode methods ids of the :mod:`stat`
|
| 100 |
+
module regarding the rwx permissions for user, group and other, special flags
|
| 101 |
+
and file system flags, such as whether it is a symlink.
|
| 102 |
+
"""
|
| 103 |
+
mode = 0
|
| 104 |
+
for iteration, char in enumerate(reversed(modestr[-6:])):
|
| 105 |
+
char = cast(Union[str, int], char)
|
| 106 |
+
mode += int(char) << iteration * 3
|
| 107 |
+
# END for each char
|
| 108 |
+
return mode
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def get_object_type_by_name(
|
| 112 |
+
object_type_name: bytes,
|
| 113 |
+
) -> Union[Type["Commit"], Type["TagObject"], Type["Tree"], Type["Blob"]]:
|
| 114 |
+
"""Retrieve the Python class GitPython uses to represent a kind of Git object.
|
| 115 |
+
|
| 116 |
+
:return:
|
| 117 |
+
A type suitable to handle the given as `object_type_name`.
|
| 118 |
+
This type can be called create new instances.
|
| 119 |
+
|
| 120 |
+
:param object_type_name:
|
| 121 |
+
Member of :attr:`Object.TYPES <git.objects.base.Object.TYPES>`.
|
| 122 |
+
|
| 123 |
+
:raise ValueError:
|
| 124 |
+
If `object_type_name` is unknown.
|
| 125 |
+
"""
|
| 126 |
+
if object_type_name == b"commit":
|
| 127 |
+
from . import commit
|
| 128 |
+
|
| 129 |
+
return commit.Commit
|
| 130 |
+
elif object_type_name == b"tag":
|
| 131 |
+
from . import tag
|
| 132 |
+
|
| 133 |
+
return tag.TagObject
|
| 134 |
+
elif object_type_name == b"blob":
|
| 135 |
+
from . import blob
|
| 136 |
+
|
| 137 |
+
return blob.Blob
|
| 138 |
+
elif object_type_name == b"tree":
|
| 139 |
+
from . import tree
|
| 140 |
+
|
| 141 |
+
return tree.Tree
|
| 142 |
+
else:
|
| 143 |
+
raise ValueError("Cannot handle unknown object type: %s" % object_type_name.decode())
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def utctz_to_altz(utctz: str) -> int:
|
| 147 |
+
"""Convert a git timezone offset into a timezone offset west of UTC in seconds
|
| 148 |
+
(compatible with :attr:`time.altzone`).
|
| 149 |
+
|
| 150 |
+
:param utctz:
|
| 151 |
+
git utc timezone string, e.g. +0200
|
| 152 |
+
"""
|
| 153 |
+
int_utctz = int(utctz)
|
| 154 |
+
seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60
|
| 155 |
+
return seconds if int_utctz < 0 else -seconds
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def altz_to_utctz_str(altz: float) -> str:
|
| 159 |
+
"""Convert a timezone offset west of UTC in seconds into a Git timezone offset
|
| 160 |
+
string.
|
| 161 |
+
|
| 162 |
+
:param altz:
|
| 163 |
+
Timezone offset in seconds west of UTC.
|
| 164 |
+
"""
|
| 165 |
+
hours = abs(altz) // 3600
|
| 166 |
+
minutes = (abs(altz) % 3600) // 60
|
| 167 |
+
sign = "-" if altz >= 60 else "+"
|
| 168 |
+
return "{}{:02}{:02}".format(sign, hours, minutes)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def verify_utctz(offset: str) -> str:
|
| 172 |
+
"""
|
| 173 |
+
:raise ValueError:
|
| 174 |
+
If `offset` is incorrect.
|
| 175 |
+
|
| 176 |
+
:return:
|
| 177 |
+
`offset`
|
| 178 |
+
"""
|
| 179 |
+
fmt_exc = ValueError("Invalid timezone offset format: %s" % offset)
|
| 180 |
+
if len(offset) != 5:
|
| 181 |
+
raise fmt_exc
|
| 182 |
+
if offset[0] not in "+-":
|
| 183 |
+
raise fmt_exc
|
| 184 |
+
if offset[1] not in digits or offset[2] not in digits or offset[3] not in digits or offset[4] not in digits:
|
| 185 |
+
raise fmt_exc
|
| 186 |
+
# END for each char
|
| 187 |
+
return offset
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class tzoffset(tzinfo):
|
| 191 |
+
def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None:
|
| 192 |
+
self._offset = timedelta(seconds=-secs_west_of_utc)
|
| 193 |
+
self._name = name or "fixed"
|
| 194 |
+
|
| 195 |
+
def __reduce__(self) -> Tuple[Type["tzoffset"], Tuple[float, str]]:
|
| 196 |
+
return tzoffset, (-self._offset.total_seconds(), self._name)
|
| 197 |
+
|
| 198 |
+
def utcoffset(self, dt: Union[datetime, None]) -> timedelta:
|
| 199 |
+
return self._offset
|
| 200 |
+
|
| 201 |
+
def tzname(self, dt: Union[datetime, None]) -> str:
|
| 202 |
+
return self._name
|
| 203 |
+
|
| 204 |
+
def dst(self, dt: Union[datetime, None]) -> timedelta:
|
| 205 |
+
return ZERO
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
utc = tzoffset(0, "UTC")
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
|
| 212 |
+
"""Convert a `timestamp` + `tz_offset` into an aware :class:`~datetime.datetime`
|
| 213 |
+
instance."""
|
| 214 |
+
utc_dt = datetime.fromtimestamp(timestamp, utc)
|
| 215 |
+
try:
|
| 216 |
+
local_dt = utc_dt.astimezone(tzoffset(tz_offset))
|
| 217 |
+
return local_dt
|
| 218 |
+
except ValueError:
|
| 219 |
+
return utc_dt
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
|
| 223 |
+
"""Parse the given date as one of the following:
|
| 224 |
+
|
| 225 |
+
* Aware datetime instance
|
| 226 |
+
* Git internal format: timestamp offset
|
| 227 |
+
* :rfc:`2822`: ``Thu, 07 Apr 2005 22:13:13 +0200``
|
| 228 |
+
* ISO 8601: ``2005-04-07T22:13:13`` - The ``T`` can be a space as well.
|
| 229 |
+
|
| 230 |
+
:return:
|
| 231 |
+
Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch
|
| 232 |
+
|
| 233 |
+
:raise ValueError:
|
| 234 |
+
If the format could not be understood.
|
| 235 |
+
|
| 236 |
+
:note:
|
| 237 |
+
Date can also be ``YYYY.MM.DD``, ``MM/DD/YYYY`` and ``DD.MM.YYYY``.
|
| 238 |
+
"""
|
| 239 |
+
if isinstance(string_date, datetime):
|
| 240 |
+
if string_date.tzinfo:
|
| 241 |
+
utcoffset = cast(timedelta, string_date.utcoffset()) # typeguard, if tzinfoand is not None
|
| 242 |
+
offset = -int(utcoffset.total_seconds())
|
| 243 |
+
return int(string_date.astimezone(utc).timestamp()), offset
|
| 244 |
+
else:
|
| 245 |
+
raise ValueError(f"string_date datetime object without tzinfo, {string_date}")
|
| 246 |
+
|
| 247 |
+
# Git time
|
| 248 |
+
try:
|
| 249 |
+
if string_date.count(" ") == 1 and string_date.rfind(":") == -1:
|
| 250 |
+
timestamp, offset_str = string_date.split()
|
| 251 |
+
if timestamp.startswith("@"):
|
| 252 |
+
timestamp = timestamp[1:]
|
| 253 |
+
timestamp_int = int(timestamp)
|
| 254 |
+
return timestamp_int, utctz_to_altz(verify_utctz(offset_str))
|
| 255 |
+
else:
|
| 256 |
+
offset_str = "+0000" # Local time by default.
|
| 257 |
+
if string_date[-5] in "-+":
|
| 258 |
+
offset_str = verify_utctz(string_date[-5:])
|
| 259 |
+
string_date = string_date[:-6] # skip space as well
|
| 260 |
+
# END split timezone info
|
| 261 |
+
offset = utctz_to_altz(offset_str)
|
| 262 |
+
|
| 263 |
+
# Now figure out the date and time portion - split time.
|
| 264 |
+
date_formats = []
|
| 265 |
+
splitter = -1
|
| 266 |
+
if "," in string_date:
|
| 267 |
+
date_formats.append("%a, %d %b %Y")
|
| 268 |
+
splitter = string_date.rfind(" ")
|
| 269 |
+
else:
|
| 270 |
+
# ISO plus additional
|
| 271 |
+
date_formats.append("%Y-%m-%d")
|
| 272 |
+
date_formats.append("%Y.%m.%d")
|
| 273 |
+
date_formats.append("%m/%d/%Y")
|
| 274 |
+
date_formats.append("%d.%m.%Y")
|
| 275 |
+
|
| 276 |
+
splitter = string_date.rfind("T")
|
| 277 |
+
if splitter == -1:
|
| 278 |
+
splitter = string_date.rfind(" ")
|
| 279 |
+
# END handle 'T' and ' '
|
| 280 |
+
# END handle RFC or ISO
|
| 281 |
+
|
| 282 |
+
assert splitter > -1
|
| 283 |
+
|
| 284 |
+
# Split date and time.
|
| 285 |
+
time_part = string_date[splitter + 1 :] # Skip space.
|
| 286 |
+
date_part = string_date[:splitter]
|
| 287 |
+
|
| 288 |
+
# Parse time.
|
| 289 |
+
tstruct = time.strptime(time_part, "%H:%M:%S")
|
| 290 |
+
|
| 291 |
+
for fmt in date_formats:
|
| 292 |
+
try:
|
| 293 |
+
dtstruct = time.strptime(date_part, fmt)
|
| 294 |
+
utctime = calendar.timegm(
|
| 295 |
+
(
|
| 296 |
+
dtstruct.tm_year,
|
| 297 |
+
dtstruct.tm_mon,
|
| 298 |
+
dtstruct.tm_mday,
|
| 299 |
+
tstruct.tm_hour,
|
| 300 |
+
tstruct.tm_min,
|
| 301 |
+
tstruct.tm_sec,
|
| 302 |
+
dtstruct.tm_wday,
|
| 303 |
+
dtstruct.tm_yday,
|
| 304 |
+
tstruct.tm_isdst,
|
| 305 |
+
)
|
| 306 |
+
)
|
| 307 |
+
return int(utctime), offset
|
| 308 |
+
except ValueError:
|
| 309 |
+
continue
|
| 310 |
+
# END exception handling
|
| 311 |
+
# END for each fmt
|
| 312 |
+
|
| 313 |
+
# Still here ? fail.
|
| 314 |
+
raise ValueError("no format matched")
|
| 315 |
+
# END handle format
|
| 316 |
+
except Exception as e:
|
| 317 |
+
raise ValueError(f"Unsupported date format or type: {string_date}, type={type(string_date)}") from e
|
| 318 |
+
# END handle exceptions
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
# Precompiled regexes
|
| 322 |
+
_re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")
|
| 323 |
+
_re_only_actor = re.compile(r"^.+? (.*)$")
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
|
| 327 |
+
"""Parse out the actor (author or committer) info from a line like::
|
| 328 |
+
|
| 329 |
+
author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
|
| 330 |
+
|
| 331 |
+
:return:
|
| 332 |
+
[Actor, int_seconds_since_epoch, int_timezone_offset]
|
| 333 |
+
"""
|
| 334 |
+
actor, epoch, offset = "", "0", "0"
|
| 335 |
+
m = _re_actor_epoch.search(line)
|
| 336 |
+
if m:
|
| 337 |
+
actor, epoch, offset = m.groups()
|
| 338 |
+
else:
|
| 339 |
+
m = _re_only_actor.search(line)
|
| 340 |
+
actor = m.group(1) if m else line or ""
|
| 341 |
+
return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset))
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# } END functions
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# { Classes
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
class ProcessStreamAdapter:
|
| 351 |
+
"""Class wiring all calls to the contained Process instance.
|
| 352 |
+
|
| 353 |
+
Use this type to hide the underlying process to provide access only to a specified
|
| 354 |
+
stream. The process is usually wrapped into an :class:`~git.cmd.Git.AutoInterrupt`
|
| 355 |
+
class to kill it if the instance goes out of scope.
|
| 356 |
+
"""
|
| 357 |
+
|
| 358 |
+
__slots__ = ("_proc", "_stream")
|
| 359 |
+
|
| 360 |
+
def __init__(self, process: "Popen", stream_name: str) -> None:
|
| 361 |
+
self._proc = process
|
| 362 |
+
self._stream: StringIO = getattr(process, stream_name) # guessed type
|
| 363 |
+
|
| 364 |
+
def __getattr__(self, attr: str) -> Any:
|
| 365 |
+
return getattr(self._stream, attr)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@runtime_checkable
|
| 369 |
+
class Traversable(Protocol):
|
| 370 |
+
"""Simple interface to perform depth-first or breadth-first traversals in one
|
| 371 |
+
direction.
|
| 372 |
+
|
| 373 |
+
Subclasses only need to implement one function.
|
| 374 |
+
|
| 375 |
+
Instances of the subclass must be hashable.
|
| 376 |
+
|
| 377 |
+
Defined subclasses:
|
| 378 |
+
|
| 379 |
+
* :class:`Commit <git.objects.Commit>`
|
| 380 |
+
* :class:`Tree <git.objects.tree.Tree>`
|
| 381 |
+
* :class:`Submodule <git.objects.submodule.base.Submodule>`
|
| 382 |
+
"""
|
| 383 |
+
|
| 384 |
+
__slots__ = ()
|
| 385 |
+
|
| 386 |
+
@classmethod
|
| 387 |
+
@abstractmethod
|
| 388 |
+
def _get_intermediate_items(cls, item: Any) -> Sequence["Traversable"]:
|
| 389 |
+
"""
|
| 390 |
+
:return:
|
| 391 |
+
Tuple of items connected to the given item.
|
| 392 |
+
Must be implemented in subclass.
|
| 393 |
+
|
| 394 |
+
class Commit:: (cls, Commit) -> Tuple[Commit, ...]
|
| 395 |
+
class Submodule:: (cls, Submodule) -> Iterablelist[Submodule]
|
| 396 |
+
class Tree:: (cls, Tree) -> Tuple[Tree, ...]
|
| 397 |
+
"""
|
| 398 |
+
raise NotImplementedError("To be implemented in subclass")
|
| 399 |
+
|
| 400 |
+
@abstractmethod
|
| 401 |
+
def list_traverse(self, *args: Any, **kwargs: Any) -> Any:
|
| 402 |
+
"""Traverse self and collect all items found.
|
| 403 |
+
|
| 404 |
+
Calling this directly on the abstract base class, including via a ``super()``
|
| 405 |
+
proxy, is deprecated. Only overridden implementations should be called.
|
| 406 |
+
"""
|
| 407 |
+
warnings.warn(
|
| 408 |
+
"list_traverse() method should only be called from subclasses."
|
| 409 |
+
" Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
|
| 410 |
+
" The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
|
| 411 |
+
DeprecationWarning,
|
| 412 |
+
stacklevel=2,
|
| 413 |
+
)
|
| 414 |
+
return self._list_traverse(*args, **kwargs)
|
| 415 |
+
|
| 416 |
+
def _list_traverse(
|
| 417 |
+
self, as_edge: bool = False, *args: Any, **kwargs: Any
|
| 418 |
+
) -> IterableList[Union["Commit", "Submodule", "Tree", "Blob"]]:
|
| 419 |
+
"""Traverse self and collect all items found.
|
| 420 |
+
|
| 421 |
+
:return:
|
| 422 |
+
:class:`~git.util.IterableList` with the results of the traversal as
|
| 423 |
+
produced by :meth:`traverse`::
|
| 424 |
+
|
| 425 |
+
Commit -> IterableList[Commit]
|
| 426 |
+
Submodule -> IterableList[Submodule]
|
| 427 |
+
Tree -> IterableList[Union[Submodule, Tree, Blob]]
|
| 428 |
+
"""
|
| 429 |
+
# Commit and Submodule have id.__attribute__ as IterableObj.
|
| 430 |
+
# Tree has id.__attribute__ inherited from IndexObject.
|
| 431 |
+
if isinstance(self, Has_id_attribute):
|
| 432 |
+
id = self._id_attribute_
|
| 433 |
+
else:
|
| 434 |
+
# Shouldn't reach here, unless Traversable subclass created with no
|
| 435 |
+
# _id_attribute_.
|
| 436 |
+
id = ""
|
| 437 |
+
# Could add _id_attribute_ to Traversable, or make all Traversable also
|
| 438 |
+
# Iterable?
|
| 439 |
+
|
| 440 |
+
if not as_edge:
|
| 441 |
+
out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
|
| 442 |
+
out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) # noqa: B026
|
| 443 |
+
return out
|
| 444 |
+
# Overloads in subclasses (mypy doesn't allow typing self: subclass).
|
| 445 |
+
# Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]]
|
| 446 |
+
else:
|
| 447 |
+
# Raise DeprecationWarning, it doesn't make sense to use this.
|
| 448 |
+
out_list: IterableList = IterableList(self.traverse(*args, **kwargs))
|
| 449 |
+
return out_list
|
| 450 |
+
|
| 451 |
+
@abstractmethod
|
| 452 |
+
def traverse(self, *args: Any, **kwargs: Any) -> Any:
|
| 453 |
+
"""Iterator yielding items found when traversing self.
|
| 454 |
+
|
| 455 |
+
Calling this directly on the abstract base class, including via a ``super()``
|
| 456 |
+
proxy, is deprecated. Only overridden implementations should be called.
|
| 457 |
+
"""
|
| 458 |
+
warnings.warn(
|
| 459 |
+
"traverse() method should only be called from subclasses."
|
| 460 |
+
" Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
|
| 461 |
+
" The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
|
| 462 |
+
DeprecationWarning,
|
| 463 |
+
stacklevel=2,
|
| 464 |
+
)
|
| 465 |
+
return self._traverse(*args, **kwargs)
|
| 466 |
+
|
| 467 |
+
def _traverse(
|
| 468 |
+
self,
|
| 469 |
+
predicate: Callable[[Union["Traversable", "Blob", TraversedTup], int], bool] = lambda i, d: True,
|
| 470 |
+
prune: Callable[[Union["Traversable", "Blob", TraversedTup], int], bool] = lambda i, d: False,
|
| 471 |
+
depth: int = -1,
|
| 472 |
+
branch_first: bool = True,
|
| 473 |
+
visit_once: bool = True,
|
| 474 |
+
ignore_self: int = 1,
|
| 475 |
+
as_edge: bool = False,
|
| 476 |
+
) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
|
| 477 |
+
"""Iterator yielding items found when traversing `self`.
|
| 478 |
+
|
| 479 |
+
:param predicate:
|
| 480 |
+
A function ``f(i,d)`` that returns ``False`` if item i at depth ``d`` should
|
| 481 |
+
not be included in the result.
|
| 482 |
+
|
| 483 |
+
:param prune:
|
| 484 |
+
A function ``f(i,d)`` that returns ``True`` if the search should stop at
|
| 485 |
+
item ``i`` at depth ``d``. Item ``i`` will not be returned.
|
| 486 |
+
|
| 487 |
+
:param depth:
|
| 488 |
+
Defines at which level the iteration should not go deeper if -1. There is no
|
| 489 |
+
limit if 0, you would effectively only get `self`, the root of the
|
| 490 |
+
iteration. If 1, you would only get the first level of
|
| 491 |
+
predecessors/successors.
|
| 492 |
+
|
| 493 |
+
:param branch_first:
|
| 494 |
+
If ``True``, items will be returned branch first, otherwise depth first.
|
| 495 |
+
|
| 496 |
+
:param visit_once:
|
| 497 |
+
If ``True``, items will only be returned once, although they might be
|
| 498 |
+
encountered several times. Loops are prevented that way.
|
| 499 |
+
|
| 500 |
+
:param ignore_self:
|
| 501 |
+
If ``True``, `self` will be ignored and automatically pruned from the
|
| 502 |
+
result. Otherwise it will be the first item to be returned. If `as_edge` is
|
| 503 |
+
``True``, the source of the first edge is ``None``.
|
| 504 |
+
|
| 505 |
+
:param as_edge:
|
| 506 |
+
If ``True``, return a pair of items, first being the source, second the
|
| 507 |
+
destination, i.e. tuple(src, dest) with the edge spanning from source to
|
| 508 |
+
destination.
|
| 509 |
+
|
| 510 |
+
:return:
|
| 511 |
+
Iterator yielding items found when traversing `self`::
|
| 512 |
+
|
| 513 |
+
Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] Submodule ->
|
| 514 |
+
Iterator[Submodule, Tuple[Submodule, Submodule]] Tree ->
|
| 515 |
+
Iterator[Union[Blob, Tree, Submodule,
|
| 516 |
+
Tuple[Union[Submodule, Tree], Union[Blob, Tree,
|
| 517 |
+
Submodule]]]
|
| 518 |
+
|
| 519 |
+
ignore_self=True is_edge=True -> Iterator[item] ignore_self=True
|
| 520 |
+
is_edge=False --> Iterator[item] ignore_self=False is_edge=True ->
|
| 521 |
+
Iterator[item] | Iterator[Tuple[src, item]] ignore_self=False
|
| 522 |
+
is_edge=False -> Iterator[Tuple[src, item]]
|
| 523 |
+
"""
|
| 524 |
+
|
| 525 |
+
visited = set()
|
| 526 |
+
stack: Deque[TraverseNT] = deque()
|
| 527 |
+
stack.append(TraverseNT(0, self, None)) # self is always depth level 0.
|
| 528 |
+
|
| 529 |
+
def addToStack(
|
| 530 |
+
stack: Deque[TraverseNT],
|
| 531 |
+
src_item: "Traversable",
|
| 532 |
+
branch_first: bool,
|
| 533 |
+
depth: int,
|
| 534 |
+
) -> None:
|
| 535 |
+
lst = self._get_intermediate_items(item)
|
| 536 |
+
if not lst: # Empty list
|
| 537 |
+
return
|
| 538 |
+
if branch_first:
|
| 539 |
+
stack.extendleft(TraverseNT(depth, i, src_item) for i in lst)
|
| 540 |
+
else:
|
| 541 |
+
reviter = (TraverseNT(depth, lst[i], src_item) for i in range(len(lst) - 1, -1, -1))
|
| 542 |
+
stack.extend(reviter)
|
| 543 |
+
|
| 544 |
+
# END addToStack local method
|
| 545 |
+
|
| 546 |
+
while stack:
|
| 547 |
+
d, item, src = stack.pop() # Depth of item, item, item_source
|
| 548 |
+
|
| 549 |
+
if visit_once and item in visited:
|
| 550 |
+
continue
|
| 551 |
+
|
| 552 |
+
if visit_once:
|
| 553 |
+
visited.add(item)
|
| 554 |
+
|
| 555 |
+
rval: Union[TraversedTup, "Traversable", "Blob"]
|
| 556 |
+
if as_edge:
|
| 557 |
+
# If as_edge return (src, item) unless rrc is None
|
| 558 |
+
# (e.g. for first item).
|
| 559 |
+
rval = (src, item)
|
| 560 |
+
else:
|
| 561 |
+
rval = item
|
| 562 |
+
|
| 563 |
+
if prune(rval, d):
|
| 564 |
+
continue
|
| 565 |
+
|
| 566 |
+
skipStartItem = ignore_self and (item is self)
|
| 567 |
+
if not skipStartItem and predicate(rval, d):
|
| 568 |
+
yield rval
|
| 569 |
+
|
| 570 |
+
# Only continue to next level if this is appropriate!
|
| 571 |
+
next_d = d + 1
|
| 572 |
+
if depth > -1 and next_d > depth:
|
| 573 |
+
continue
|
| 574 |
+
|
| 575 |
+
addToStack(stack, item, branch_first, next_d)
|
| 576 |
+
# END for each item on work stack
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
@runtime_checkable
|
| 580 |
+
class Serializable(Protocol):
|
| 581 |
+
"""Defines methods to serialize and deserialize objects from and into a data
|
| 582 |
+
stream."""
|
| 583 |
+
|
| 584 |
+
__slots__ = ()
|
| 585 |
+
|
| 586 |
+
# @abstractmethod
|
| 587 |
+
def _serialize(self, stream: "BytesIO") -> "Serializable":
|
| 588 |
+
"""Serialize the data of this object into the given data stream.
|
| 589 |
+
|
| 590 |
+
:note:
|
| 591 |
+
A serialized object would :meth:`_deserialize` into the same object.
|
| 592 |
+
|
| 593 |
+
:param stream:
|
| 594 |
+
A file-like object.
|
| 595 |
+
|
| 596 |
+
:return:
|
| 597 |
+
self
|
| 598 |
+
"""
|
| 599 |
+
raise NotImplementedError("To be implemented in subclass")
|
| 600 |
+
|
| 601 |
+
# @abstractmethod
|
| 602 |
+
def _deserialize(self, stream: "BytesIO") -> "Serializable":
|
| 603 |
+
"""Deserialize all information regarding this object from the stream.
|
| 604 |
+
|
| 605 |
+
:param stream:
|
| 606 |
+
A file-like object.
|
| 607 |
+
|
| 608 |
+
:return:
|
| 609 |
+
self
|
| 610 |
+
"""
|
| 611 |
+
raise NotImplementedError("To be implemented in subclass")
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
class TraversableIterableObj(IterableObj, Traversable):
|
| 615 |
+
__slots__ = ()
|
| 616 |
+
|
| 617 |
+
TIobj_tuple = Tuple[Union[T_TIobj, None], T_TIobj]
|
| 618 |
+
|
| 619 |
+
def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]:
|
| 620 |
+
return super()._list_traverse(*args, **kwargs)
|
| 621 |
+
|
| 622 |
+
@overload
|
| 623 |
+
def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ...
|
| 624 |
+
|
| 625 |
+
@overload
|
| 626 |
+
def traverse(
|
| 627 |
+
self: T_TIobj,
|
| 628 |
+
predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
|
| 629 |
+
prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
|
| 630 |
+
depth: int,
|
| 631 |
+
branch_first: bool,
|
| 632 |
+
visit_once: bool,
|
| 633 |
+
ignore_self: Literal[True],
|
| 634 |
+
as_edge: Literal[False],
|
| 635 |
+
) -> Iterator[T_TIobj]: ...
|
| 636 |
+
|
| 637 |
+
@overload
|
| 638 |
+
def traverse(
|
| 639 |
+
self: T_TIobj,
|
| 640 |
+
predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
|
| 641 |
+
prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool],
|
| 642 |
+
depth: int,
|
| 643 |
+
branch_first: bool,
|
| 644 |
+
visit_once: bool,
|
| 645 |
+
ignore_self: Literal[False],
|
| 646 |
+
as_edge: Literal[True],
|
| 647 |
+
) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: ...
|
| 648 |
+
|
| 649 |
+
@overload
|
| 650 |
+
def traverse(
|
| 651 |
+
self: T_TIobj,
|
| 652 |
+
predicate: Callable[[Union[T_TIobj, TIobj_tuple], int], bool],
|
| 653 |
+
prune: Callable[[Union[T_TIobj, TIobj_tuple], int], bool],
|
| 654 |
+
depth: int,
|
| 655 |
+
branch_first: bool,
|
| 656 |
+
visit_once: bool,
|
| 657 |
+
ignore_self: Literal[True],
|
| 658 |
+
as_edge: Literal[True],
|
| 659 |
+
) -> Iterator[Tuple[T_TIobj, T_TIobj]]: ...
|
| 660 |
+
|
| 661 |
+
def traverse(
|
| 662 |
+
self: T_TIobj,
|
| 663 |
+
predicate: Callable[[Union[T_TIobj, TIobj_tuple], int], bool] = lambda i, d: True,
|
| 664 |
+
prune: Callable[[Union[T_TIobj, TIobj_tuple], int], bool] = lambda i, d: False,
|
| 665 |
+
depth: int = -1,
|
| 666 |
+
branch_first: bool = True,
|
| 667 |
+
visit_once: bool = True,
|
| 668 |
+
ignore_self: int = 1,
|
| 669 |
+
as_edge: bool = False,
|
| 670 |
+
) -> Union[Iterator[T_TIobj], Iterator[Tuple[T_TIobj, T_TIobj]], Iterator[TIobj_tuple]]:
|
| 671 |
+
"""For documentation, see :meth:`Traversable._traverse`."""
|
| 672 |
+
|
| 673 |
+
## To typecheck instead of using cast:
|
| 674 |
+
#
|
| 675 |
+
# import itertools
|
| 676 |
+
# from git.types import TypeGuard
|
| 677 |
+
# def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit', 'Commit']]]]:
|
| 678 |
+
# for x in inp[1]:
|
| 679 |
+
# if not isinstance(x, tuple) and len(x) != 2:
|
| 680 |
+
# if all(isinstance(inner, Commit) for inner in x):
|
| 681 |
+
# continue
|
| 682 |
+
# return True
|
| 683 |
+
#
|
| 684 |
+
# ret = super(Commit, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge)
|
| 685 |
+
# ret_tup = itertools.tee(ret, 2)
|
| 686 |
+
# assert is_commit_traversed(ret_tup), f"{[type(x) for x in list(ret_tup[0])]}"
|
| 687 |
+
# return ret_tup[0]
|
| 688 |
+
|
| 689 |
+
return cast(
|
| 690 |
+
Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
|
| 691 |
+
super()._traverse(
|
| 692 |
+
predicate, # type: ignore[arg-type]
|
| 693 |
+
prune, # type: ignore[arg-type]
|
| 694 |
+
depth,
|
| 695 |
+
branch_first,
|
| 696 |
+
visit_once,
|
| 697 |
+
ignore_self,
|
| 698 |
+
as_edge,
|
| 699 |
+
),
|
| 700 |
+
)
|