git-project 0.0.36__tar.gz → 0.0.38__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_project-0.0.36 → git_project-0.0.38}/.gitignore +22 -0
- git_project-0.0.38/PKG-INFO +155 -0
- {git_project-0.0.36 → git_project-0.0.38}/_version.py +2 -2
- git_project-0.0.38/docs/changelog.rst +84 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/intro.rst +1 -1
- {git_project-0.0.36 → git_project-0.0.38}/pyproject.toml +41 -9
- {git_project-0.0.36 → git_project-0.0.38}/requirements.txt +1 -1
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/commandline.py +1 -5
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/git.py +90 -60
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/main.py +5 -1
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/pluginmanager.py +1 -5
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/project.py +10 -5
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_git.py +147 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_project.py +29 -8
- git_project-0.0.36/PKG-INFO +0 -78
- git_project-0.0.36/build/lib/git_project/__init__.py +0 -33
- git_project-0.0.36/build/lib/git_project/commandline.py +0 -164
- git_project-0.0.36/build/lib/git_project/configobj.py +0 -300
- git_project-0.0.36/build/lib/git_project/exception.py +0 -30
- git_project-0.0.36/build/lib/git_project/git.py +0 -873
- git_project-0.0.36/build/lib/git_project/gitproject.py +0 -61
- git_project-0.0.36/build/lib/git_project/main.py +0 -63
- git_project-0.0.36/build/lib/git_project/parsermanager.py +0 -360
- git_project-0.0.36/build/lib/git_project/plugin.py +0 -118
- git_project-0.0.36/build/lib/git_project/pluginmanager.py +0 -54
- git_project-0.0.36/build/lib/git_project/project.py +0 -197
- git_project-0.0.36/build/lib/git_project/runnable.py +0 -71
- git_project-0.0.36/build/lib/git_project/scopedobj.py +0 -207
- git_project-0.0.36/build/lib/git_project/shell.py +0 -119
- git_project-0.0.36/build/lib/git_project/substitutable.py +0 -139
- git_project-0.0.36/docs/changelog.rst +0 -39
- git_project-0.0.36/test.out +0 -18
- {git_project-0.0.36 → git_project-0.0.38}/CONTRIBUTORS +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/COPYING +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/MANIFEST.in +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/Makefile +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/authors.rst +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/conf.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/index.rst +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/license.rst +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/docs/make.bat +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/__init__.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/configobj.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/exception.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/gitproject.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/parsermanager.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/plugin.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/runnable.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/scopedobj.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/shell.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/substitutable.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/test_support/__init__.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/src/git_project/test_support/common.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/conftest.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_confobj.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_gitproject.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_main.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_runnable.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_scopedobj.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_shell.py +0 -0
- {git_project-0.0.36 → git_project-0.0.38}/tests/test_substitutable.py +0 -0
|
@@ -26,3 +26,25 @@
|
|
|
26
26
|
|
|
27
27
|
# Python egg metadata, regenerated from source files by setuptools.
|
|
28
28
|
/*.egg-info
|
|
29
|
+
|
|
30
|
+
# Python bytecode cache directories.
|
|
31
|
+
__pycache__/
|
|
32
|
+
|
|
33
|
+
# Version file generated by vcs-versioning.
|
|
34
|
+
/_version.py
|
|
35
|
+
|
|
36
|
+
# Setuptools build folder.
|
|
37
|
+
/build/
|
|
38
|
+
|
|
39
|
+
# Editor backup files.
|
|
40
|
+
*~
|
|
41
|
+
|
|
42
|
+
# Coverage data. The coverage config sets parallel, so each run also writes
|
|
43
|
+
# .coverage.<host>.<pid>.<random> files for coverage combine.
|
|
44
|
+
.coverage*
|
|
45
|
+
|
|
46
|
+
# Coverage HTML report.
|
|
47
|
+
/htmlcov/
|
|
48
|
+
|
|
49
|
+
# Sphinx build output.
|
|
50
|
+
/docs/_build/
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: git-project
|
|
3
|
+
Version: 0.0.38
|
|
4
|
+
Summary: The extensible stupid project manager
|
|
5
|
+
Author-email: "David A. Greene" <dag@obbligato.org>
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
License-File: COPYING
|
|
8
|
+
Keywords: development,git,project
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: progressbar2>=4.4.2
|
|
18
|
+
Requires-Dist: pygit2>=1.18.2
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-console-scripts; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
23
|
+
Requires-Dist: setuptools; extra == 'test'
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
===================================================
|
|
28
|
+
git-project - The extensible stupid project manager
|
|
29
|
+
===================================================
|
|
30
|
+
|
|
31
|
+
|VersionImageLink|_
|
|
32
|
+
|
|
33
|
+
|PythonVersionImageLink|_
|
|
34
|
+
|
|
35
|
+
.. |VersionImageLink| image:: https://img.shields.io/pypi/v/git-project.svg
|
|
36
|
+
.. _VersionImageLink: https://pypi.org/project/git-project
|
|
37
|
+
.. |PythonVersionImageLink| image:: https://img.shields.io/pypi/pyversions/git-project.svg
|
|
38
|
+
.. _PythonVersionImageLink: https://pypi.org/project/git-project
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
-------
|
|
42
|
+
|
|
43
|
+
.. contents:: Table of Contents
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Installation
|
|
47
|
+
============
|
|
48
|
+
::
|
|
49
|
+
|
|
50
|
+
pip install git-project
|
|
51
|
+
pip install git-project-core-plugins
|
|
52
|
+
|
|
53
|
+
Description
|
|
54
|
+
===========
|
|
55
|
+
|
|
56
|
+
git-project is a git extension to manage development of a project hosted in a
|
|
57
|
+
git repository. By itself git-project does almost nothing. Its functionality
|
|
58
|
+
is enhanced by plugins.
|
|
59
|
+
|
|
60
|
+
`git-project-core-plugins
|
|
61
|
+
<http://www.github.com/greened/git-project-core-plugins>`_ provides a set of
|
|
62
|
+
basic functionality and should almost always be installed alongside git-project.
|
|
63
|
+
|
|
64
|
+
Conventions
|
|
65
|
+
===========
|
|
66
|
+
|
|
67
|
+
Symlinks identiy projects to the ``git-project`` command. For example, if
|
|
68
|
+
``git-fizzbin`` is symlinked to ``git-project``, then ``git fizzbin <command>``
|
|
69
|
+
will invoke ``git-project`` with ``fizzbin`` as the "active project." To
|
|
70
|
+
emphasize this, we show git-project commands with a generic ``<project>``
|
|
71
|
+
identifier::
|
|
72
|
+
|
|
73
|
+
git <project> --help
|
|
74
|
+
|
|
75
|
+
Discussion
|
|
76
|
+
==========
|
|
77
|
+
|
|
78
|
+
With git-project and its core plugins you can:
|
|
79
|
+
|
|
80
|
+
* Initialize a development environment at clone time (or after clone time)
|
|
81
|
+
* Manage branches
|
|
82
|
+
* Manage worktrees
|
|
83
|
+
* Set and invoke commands
|
|
84
|
+
|
|
85
|
+
git-project is intended to make switching between active 'tasks' in a repository
|
|
86
|
+
simple and fast, without losing the progress context of existing tasks. For
|
|
87
|
+
example the core plugins set up build environments such that switching among
|
|
88
|
+
projects and worktrees does not result in "rebuilding the world." Builds can be
|
|
89
|
+
configured to invoke complex commands via a convenient name (e.g. ``git
|
|
90
|
+
<project> build debug``)
|
|
91
|
+
|
|
92
|
+
Substitution variables
|
|
93
|
+
======================
|
|
94
|
+
|
|
95
|
+
Commands that allow substitution take a form ``{varname}`` in their configured
|
|
96
|
+
textual representation and substitute it with the value of ``varname``.
|
|
97
|
+
``varname`` can be any configured value under ``<project>``, for example::
|
|
98
|
+
|
|
99
|
+
[project]
|
|
100
|
+
myvar = value
|
|
101
|
+
|
|
102
|
+
``git-project`` has several built-in substitution variables that various
|
|
103
|
+
commands and plugins can use:
|
|
104
|
+
|
|
105
|
+
``branch``
|
|
106
|
+
The name of the currently checked-out branch
|
|
107
|
+
``gitdir``
|
|
108
|
+
The value of ``GITDIR``
|
|
109
|
+
``git_common_dir``
|
|
110
|
+
The value of ``GIT_COMMON_DIR``
|
|
111
|
+
``project``
|
|
112
|
+
The value of ``<project>``
|
|
113
|
+
|
|
114
|
+
License
|
|
115
|
+
=======
|
|
116
|
+
`git-project` is distributed under the terms of the `GNU General Public License v3.0 or later`_.
|
|
117
|
+
|
|
118
|
+
.. _`GNU General Public License v3.0 or later`: https://spdx.org/licenses/GPL-3.0-or-later.html
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
..
|
|
122
|
+
SPDX-FileCopyrightText: 2024-present David A. Greene <dag@obbligato.org>
|
|
123
|
+
|
|
124
|
+
..
|
|
125
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
126
|
+
|
|
127
|
+
..
|
|
128
|
+
Copyright 2023 David A. Greene
|
|
129
|
+
|
|
130
|
+
..
|
|
131
|
+
This file is part of git-project
|
|
132
|
+
|
|
133
|
+
..
|
|
134
|
+
git-project is free software: you can redistribute it and/or modify it under
|
|
135
|
+
the terms of the GNU Affero General Public License as published by the Free
|
|
136
|
+
Software Foundation, either version 3 of the License, or (at your option)
|
|
137
|
+
any later version.
|
|
138
|
+
|
|
139
|
+
..
|
|
140
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
141
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
142
|
+
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
143
|
+
more details.
|
|
144
|
+
|
|
145
|
+
..
|
|
146
|
+
You should have received a copy of the GNU Affero General Public License
|
|
147
|
+
along with git-project. If not, see <https://www.gnu.org/licenses/>.
|
|
148
|
+
|
|
149
|
+
Authors
|
|
150
|
+
=======
|
|
151
|
+
*git-project* is written and maintained by |author|.
|
|
152
|
+
|
|
153
|
+
.. |author| replace:: `David Greene`_
|
|
154
|
+
.. _David Greene: https://github.com/greened
|
|
155
|
+
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
21
|
+
__version__ = version = '0.0.38'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 38)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
..
|
|
2
|
+
SPDX-FileCopyrightText: 2023-present David A. Greene <dag@obbligato.org>
|
|
3
|
+
|
|
4
|
+
..
|
|
5
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
..
|
|
8
|
+
Copyright 2023 David A. Greene
|
|
9
|
+
|
|
10
|
+
..
|
|
11
|
+
This file is part of git-project
|
|
12
|
+
|
|
13
|
+
..
|
|
14
|
+
git-project is free software: you can redistribute it and/or modify it under
|
|
15
|
+
the terms of the GNU Affero General Public License as published by the Free
|
|
16
|
+
Software Foundation, either version 3 of the License, or (at your option)
|
|
17
|
+
any later version.
|
|
18
|
+
|
|
19
|
+
..
|
|
20
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
21
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
23
|
+
more details.
|
|
24
|
+
|
|
25
|
+
..
|
|
26
|
+
You should have received a copy of the GNU Affero General Public License
|
|
27
|
+
along with git-project. If not, see <https://www.gnu.org/licenses/>.
|
|
28
|
+
|
|
29
|
+
ChangeLog
|
|
30
|
+
=========
|
|
31
|
+
`Unreleased`_
|
|
32
|
+
-------------
|
|
33
|
+
|
|
34
|
+
`0.0.38`_ - 2026-09-24
|
|
35
|
+
----------------------
|
|
36
|
+
Added
|
|
37
|
+
.....
|
|
38
|
+
- ``prune_branch`` takes a ``keep_remote_branch`` keyword. It previously
|
|
39
|
+
deleted the branch from every project remote as well as locally, so retiring
|
|
40
|
+
a local branch destroyed the remote copy with it.
|
|
41
|
+
|
|
42
|
+
Changed
|
|
43
|
+
.......
|
|
44
|
+
- Python 3.10 or later is now required. ``list_heads`` needs it, and 1.15.1 is
|
|
45
|
+
the last pygit2 that supports 3.9, so on 3.9 there is no installable pygit2
|
|
46
|
+
carrying the API.
|
|
47
|
+
- pygit2 1.18.2 or later is now required, up from 1.15.1. The old range still
|
|
48
|
+
resolved, so the package installed cleanly and then raised ``AttributeError``
|
|
49
|
+
in use.
|
|
50
|
+
|
|
51
|
+
Fixed
|
|
52
|
+
.....
|
|
53
|
+
- ``git <project> clone`` did not work at all. ``Git.clone`` required an ssh ID,
|
|
54
|
+
so the clone paths raised ``TypeError``. The ID is now optional, and a
|
|
55
|
+
keypair is simply not offered when there is none. This also fixes
|
|
56
|
+
``fetch_remote``, ``remote_branch_exists`` and ``delete_remote_branch`` for
|
|
57
|
+
any repository that never set ``ssh.id``.
|
|
58
|
+
- ``remote_branch_exists`` called ``Remote.ls_remotes()``, which pygit2 1.20
|
|
59
|
+
removed. It now calls ``list_heads()`` and reads the fields off the
|
|
60
|
+
``RemoteHead`` objects.
|
|
61
|
+
- A ``Git`` re-pointed at a path with no repository stayed attached to the
|
|
62
|
+
previous one. ``has_repo`` answered ``True``, so later work ran against the
|
|
63
|
+
repository the caller had left. It now detaches, and ``has_repo`` answers
|
|
64
|
+
``False``.
|
|
65
|
+
- A ``Git`` built outside any repository named a missing private field rather
|
|
66
|
+
than the real fault. ``reinit`` set the repository only when discovery
|
|
67
|
+
succeeded, so most accessors raised ``AttributeError``. ``config`` named
|
|
68
|
+
``_config`` rather than ``_repo``, and ``committish_exists`` raised nothing
|
|
69
|
+
at all and answered ``False``, which said the committish was absent. The
|
|
70
|
+
repository accessors now raise ``GitProjectException`` naming the path that
|
|
71
|
+
was searched.
|
|
72
|
+
- The Issues link on the PyPI project page returned 404. The URL named
|
|
73
|
+
``unknown/greened`` instead of ``greened/git-project``, a leftover from
|
|
74
|
+
hatch's project template.
|
|
75
|
+
- The PyPI long description stopped at the separator after the badges, so
|
|
76
|
+
everything from Installation on went unpublished. A second readme fragment
|
|
77
|
+
now carries the rest.
|
|
78
|
+
- The Sphinx docs rendered no module documentation at all. ``automodule`` was
|
|
79
|
+
asked for ``git-project``, which is not an importable name; it now asks for
|
|
80
|
+
``git_project``. No declared environment supplied Sphinx either, so a
|
|
81
|
+
``docs`` environment with Sphinx is now declared.
|
|
82
|
+
|
|
83
|
+
.. _Unreleased: https://github.com/greened/git-project/compare/v0.0.38...HEAD
|
|
84
|
+
.. _0.0.38: https://github.com/greened/git-project/compare/v0.0.37...v0.0.38
|
|
@@ -31,7 +31,7 @@ name = "git-project"
|
|
|
31
31
|
dynamic = ["dependencies", "readme", "version"]
|
|
32
32
|
description = "The extensible stupid project manager"
|
|
33
33
|
license = "AGPL-3.0-or-later"
|
|
34
|
-
requires-python = ">=3.
|
|
34
|
+
requires-python = ">=3.10"
|
|
35
35
|
authors = [
|
|
36
36
|
{ name = "David A. Greene", email = "dag@obbligato.org" },
|
|
37
37
|
]
|
|
@@ -45,7 +45,8 @@ classifiers = [
|
|
|
45
45
|
"Environment :: Console",
|
|
46
46
|
"Intended Audience :: Developers",
|
|
47
47
|
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
|
48
|
-
"Programming Language :: Python :: 3.
|
|
48
|
+
"Programming Language :: Python :: 3.10",
|
|
49
|
+
"Programming Language :: Python :: 3.11",
|
|
49
50
|
"Topic :: Software Development :: Version Control :: Git",
|
|
50
51
|
]
|
|
51
52
|
|
|
@@ -62,7 +63,7 @@ git-project = "git_project.main:main"
|
|
|
62
63
|
|
|
63
64
|
[project.urls]
|
|
64
65
|
Documentation = "https://github.com/greened/git-project#readme"
|
|
65
|
-
Issues = "https://github.com/
|
|
66
|
+
Issues = "https://github.com/greened/git-project/issues"
|
|
66
67
|
Source = "https://github.com/greened/git-project"
|
|
67
68
|
|
|
68
69
|
[tool.hatch.version]
|
|
@@ -70,6 +71,9 @@ source = "vcs"
|
|
|
70
71
|
|
|
71
72
|
[tool.hatch.envs.default]
|
|
72
73
|
dependencies = [
|
|
74
|
+
# Provides the "git-project.plugins" entry points the CLI tests dispatch
|
|
75
|
+
# through; without them argparse offers an empty subcommand list.
|
|
76
|
+
"git-project-core-plugins",
|
|
73
77
|
"coverage[toml]>=6.5",
|
|
74
78
|
"pytest",
|
|
75
79
|
"pytest-console-scripts",
|
|
@@ -88,7 +92,7 @@ cov = [
|
|
|
88
92
|
]
|
|
89
93
|
|
|
90
94
|
[[tool.hatch.envs.all.matrix]]
|
|
91
|
-
python = ["3.
|
|
95
|
+
python = ["3.10", "3.11"]
|
|
92
96
|
|
|
93
97
|
[tool.hatch.envs.lint]
|
|
94
98
|
detached = true
|
|
@@ -113,14 +117,27 @@ all = [
|
|
|
113
117
|
"typing",
|
|
114
118
|
]
|
|
115
119
|
|
|
120
|
+
[tool.hatch.envs.docs]
|
|
121
|
+
# NOT detached: autodoc imports the package, because docs/intro.rst pulls the
|
|
122
|
+
# module docstring in with automodule rather than keeping a second copy. A
|
|
123
|
+
# detached env would build with the package absent and the directive would fail.
|
|
124
|
+
dependencies = [
|
|
125
|
+
"sphinx>=7",
|
|
126
|
+
]
|
|
127
|
+
[tool.hatch.envs.docs.scripts]
|
|
128
|
+
build = "sphinx-build -M html docs docs/_build"
|
|
129
|
+
clean = "sphinx-build -M clean docs docs/_build"
|
|
130
|
+
|
|
116
131
|
[tool.black]
|
|
117
|
-
target-version = ["
|
|
132
|
+
target-version = ["py310"]
|
|
118
133
|
line-length = 120
|
|
119
134
|
skip-string-normalization = true
|
|
120
135
|
|
|
121
136
|
[tool.ruff]
|
|
122
|
-
target-version = "
|
|
137
|
+
target-version = "py310"
|
|
123
138
|
line-length = 120
|
|
139
|
+
|
|
140
|
+
[tool.ruff.lint]
|
|
124
141
|
select = [
|
|
125
142
|
"A",
|
|
126
143
|
"ARG",
|
|
@@ -163,13 +180,13 @@ unfixable = [
|
|
|
163
180
|
"F401",
|
|
164
181
|
]
|
|
165
182
|
|
|
166
|
-
[tool.ruff.isort]
|
|
183
|
+
[tool.ruff.lint.isort]
|
|
167
184
|
known-first-party = ["git-project"]
|
|
168
185
|
|
|
169
|
-
[tool.ruff.flake8-tidy-imports]
|
|
186
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
170
187
|
ban-relative-imports = "all"
|
|
171
188
|
|
|
172
|
-
[tool.ruff.per-file-ignores]
|
|
189
|
+
[tool.ruff.lint.per-file-ignores]
|
|
173
190
|
# Tests can use magic values, assertions, and relative imports
|
|
174
191
|
"tests/**/*" = ["PLR2004", "S101", "TID252"]
|
|
175
192
|
|
|
@@ -213,6 +230,21 @@ text = """
|
|
|
213
230
|
|
|
214
231
|
"""
|
|
215
232
|
|
|
233
|
+
# The rest of the module docstring: everything past the badges. Without this
|
|
234
|
+
# the description stopped at the separator above, so Installation and the whole
|
|
235
|
+
# body were never published. The docstring is the single source; docs/intro.rst
|
|
236
|
+
# pulls the same text in with automodule.
|
|
237
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
238
|
+
path = "src/git_project/__init__.py"
|
|
239
|
+
start-after = "-------\n"
|
|
240
|
+
end-before = "\"\"\""
|
|
241
|
+
|
|
242
|
+
# Fragments are concatenated verbatim and the docstring ends in an indented
|
|
243
|
+
# block, so without a blank line the next fragment unindents into it. That is an
|
|
244
|
+
# RST error and PyPI rejects a description that fails to render.
|
|
245
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
246
|
+
text = "\n"
|
|
247
|
+
|
|
216
248
|
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
217
249
|
path = "docs/authors.rst"
|
|
218
250
|
end-before = "A full list of contributors"
|
|
@@ -47,11 +47,7 @@ import subprocess
|
|
|
47
47
|
import sys
|
|
48
48
|
import urllib.parse
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
from importlib import metadata
|
|
52
|
-
except ImportError:
|
|
53
|
-
# Running on pre-3.8 Python; use importlib-metadata package
|
|
54
|
-
import importlib_metadata as metadata
|
|
50
|
+
from importlib import metadata
|
|
55
51
|
|
|
56
52
|
from .parsermanager import ParserManager
|
|
57
53
|
|
|
@@ -32,23 +32,32 @@ import urllib
|
|
|
32
32
|
from .exception import GitProjectException
|
|
33
33
|
from .shell import capture_command
|
|
34
34
|
|
|
35
|
-
# Python 3.9 things so we don't actually need 3.9.
|
|
36
|
-
|
|
37
|
-
def _is_relative_to(path, other):
|
|
38
|
-
try:
|
|
39
|
-
path.relative_to(other)
|
|
40
|
-
return True
|
|
41
|
-
except ValueError:
|
|
42
|
-
return False
|
|
43
|
-
|
|
44
35
|
# Git commands
|
|
45
36
|
#
|
|
46
37
|
class Git(object):
|
|
47
38
|
"""A facade over a lower-level interface to git providing interfaces needed to
|
|
48
39
|
implement git-project functionality."""
|
|
49
40
|
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _ssh_credentials(ssh_id, username_from_url, allowed_types):
|
|
43
|
+
"""Return the credentials to authenticate with, or None if we have none to
|
|
44
|
+
offer. Name the keypair under ~/.ssh for the given ssh ID. Offer no
|
|
45
|
+
keypair without an ssh ID.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
if ssh_id and allowed_types & pygit2.enums.CredentialType.SSH_KEY:
|
|
49
|
+
return pygit2.Keypair(
|
|
50
|
+
username_from_url,
|
|
51
|
+
str(Path.home() / '.ssh' / f'{ssh_id}.pub'),
|
|
52
|
+
str(Path.home() / '.ssh' / ssh_id),
|
|
53
|
+
''
|
|
54
|
+
)
|
|
55
|
+
elif allowed_types & pygit2.enums.CredentialType.USERNAME:
|
|
56
|
+
return pygit2.Username(username_from_url)
|
|
57
|
+
return None
|
|
58
|
+
|
|
50
59
|
class RemoteCallbacks(pygit2.RemoteCallbacks):
|
|
51
|
-
def __init__(self, ssh_id: str):
|
|
60
|
+
def __init__(self, ssh_id: str | None):
|
|
52
61
|
self.started_transfer = False
|
|
53
62
|
self.transfer_done = False
|
|
54
63
|
|
|
@@ -60,16 +69,8 @@ class Git(object):
|
|
|
60
69
|
self.ssh_id = ssh_id
|
|
61
70
|
|
|
62
71
|
def credentials(self, url, username_from_url, allowed_types):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
username_from_url,
|
|
66
|
-
str(Path.home() / '.ssh' / f'{self.ssh_id}.pub'),
|
|
67
|
-
str(Path.home() / '.ssh' / self.ssh_id),
|
|
68
|
-
''
|
|
69
|
-
)
|
|
70
|
-
elif allowed_types & pygit2.enums.CredentialType.USERNAME:
|
|
71
|
-
return pygit2.Username(username_from_url)
|
|
72
|
-
return None
|
|
72
|
+
return Git._ssh_credentials(self.ssh_id, username_from_url,
|
|
73
|
+
allowed_types)
|
|
73
74
|
|
|
74
75
|
def sideband_progress(self, message):
|
|
75
76
|
print(f'Remote: {message}')
|
|
@@ -377,21 +378,13 @@ class Git(object):
|
|
|
377
378
|
del self._sections[section_name]
|
|
378
379
|
|
|
379
380
|
class RemoteBranchDeleteCallback(pygit2.RemoteCallbacks):
|
|
380
|
-
def __init__(self, ssh_id: str):
|
|
381
|
+
def __init__(self, ssh_id: str | None):
|
|
381
382
|
self.ssh_id = ssh_id
|
|
382
383
|
|
|
383
384
|
"""Check the result of remove branch prune operations."""
|
|
384
385
|
def credentials(self, url, username_from_url, allowed_types):
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
username_from_url,
|
|
388
|
-
str(Path.home() / '.ssh' / f'{self.ssh_id}.pub'),
|
|
389
|
-
str(Path.home() / '.ssh' / self.ssh_id),
|
|
390
|
-
''
|
|
391
|
-
)
|
|
392
|
-
elif allowed_types & pygit2.enums.CredentialType.USERNAME:
|
|
393
|
-
return pygit2.Username(username_from_url)
|
|
394
|
-
return None
|
|
386
|
+
return Git._ssh_credentials(self.ssh_id, username_from_url,
|
|
387
|
+
allowed_types)
|
|
395
388
|
|
|
396
389
|
def push_update_reference(self, refname, message):
|
|
397
390
|
if message is not None:
|
|
@@ -399,40 +392,66 @@ class Git(object):
|
|
|
399
392
|
format(message))
|
|
400
393
|
|
|
401
394
|
class LsRemotesCallbacks(pygit2.RemoteCallbacks):
|
|
402
|
-
def __init__(self, ssh_id: str):
|
|
395
|
+
def __init__(self, ssh_id: str | None):
|
|
403
396
|
self.ssh_id = ssh_id
|
|
404
397
|
|
|
405
398
|
def credentials(self, url, username_from_url, allowed_types):
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
username_from_url,
|
|
409
|
-
str(Path.home() / '.ssh' / f'{self.ssh_id}.pub'),
|
|
410
|
-
str(Path.home() / '.ssh' / self.ssh_id),
|
|
411
|
-
''
|
|
412
|
-
)
|
|
413
|
-
elif allowed_types & pygit2.enums.CredentialType.USERNAME:
|
|
414
|
-
return pygit2.Username(username_from_url)
|
|
415
|
-
return None
|
|
399
|
+
return Git._ssh_credentials(self.ssh_id, username_from_url,
|
|
400
|
+
allowed_types)
|
|
416
401
|
|
|
417
402
|
# Repository-wide info
|
|
418
403
|
def __init__(self):
|
|
419
404
|
self.reinit(Path.cwd())
|
|
420
405
|
|
|
421
|
-
def reinit(self, gitdir: Path):
|
|
422
|
-
|
|
406
|
+
def reinit(self, gitdir: Path | str):
|
|
407
|
+
"""Attach to the repository that holds gitdir, or to no repository.
|
|
408
|
+
|
|
409
|
+
A caller may build a Git outside any repository. Record the path we
|
|
410
|
+
searched and leave the repository empty. has_repo then answers False,
|
|
411
|
+
and each repository accessor raises an error that names the path.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
self._search_path = Path(gitdir).resolve()
|
|
415
|
+
repo_path = pygit2.discover_repository(self._search_path)
|
|
423
416
|
if repo_path:
|
|
424
|
-
self.
|
|
425
|
-
self._config = self.Config(self, self.
|
|
417
|
+
self._repository = pygit2.Repository(repo_path)
|
|
418
|
+
self._config = self.Config(self, self._repository.config)
|
|
426
419
|
self.validate_config()
|
|
420
|
+
else:
|
|
421
|
+
self._repository = None
|
|
422
|
+
self._config = None
|
|
423
|
+
|
|
424
|
+
def _no_repository_error(self):
|
|
425
|
+
"""Return the error to raise when there is no repository."""
|
|
426
|
+
return GitProjectException(
|
|
427
|
+
f'No git repository at {self._search_path} or any parent')
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def _repo(self):
|
|
431
|
+
"""Return the pygit2 repository. Raise if there is none.
|
|
432
|
+
|
|
433
|
+
Each accessor reads the repository through this property, so one guard
|
|
434
|
+
gives them all the same legible error.
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
if self._repository is None:
|
|
438
|
+
raise self._no_repository_error()
|
|
439
|
+
return self._repository
|
|
427
440
|
|
|
428
441
|
@property
|
|
429
442
|
def config(self):
|
|
430
|
-
"""Return the config of the repository.
|
|
443
|
+
"""Return the config of the repository.
|
|
444
|
+
|
|
445
|
+
Raise GitProjectException when there is no repository.
|
|
446
|
+
|
|
447
|
+
"""
|
|
448
|
+
if self._repository is None or self._config is None:
|
|
449
|
+
raise self._no_repository_error()
|
|
431
450
|
return self._config
|
|
432
451
|
|
|
433
452
|
def has_repo(self):
|
|
434
453
|
"""Return whether we are attached to a repository."""
|
|
435
|
-
return
|
|
454
|
+
return self._repository is not None
|
|
436
455
|
|
|
437
456
|
def reload_config(self):
|
|
438
457
|
"""Reload the config."""
|
|
@@ -509,7 +528,7 @@ class Git(object):
|
|
|
509
528
|
for name in self._repo.list_worktrees():
|
|
510
529
|
worktree = self._repo.lookup_worktree(name)
|
|
511
530
|
path = Path(worktree.path).resolve()
|
|
512
|
-
if path == cwd or
|
|
531
|
+
if path == cwd or cwd.is_relative_to(path):
|
|
513
532
|
return name
|
|
514
533
|
|
|
515
534
|
return None
|
|
@@ -522,13 +541,13 @@ class Git(object):
|
|
|
522
541
|
for name in self._repo.list_worktrees():
|
|
523
542
|
worktree = self._repo.lookup_worktree(name)
|
|
524
543
|
path = Path(worktree.path).resolve()
|
|
525
|
-
if path == cwd or
|
|
544
|
+
if path == cwd or cwd.is_relative_to(path):
|
|
526
545
|
return str(path)
|
|
527
546
|
|
|
528
547
|
# See if we have a GITDIR somewhere along the way.
|
|
529
548
|
gitdir = Path(self._repo.path).resolve()
|
|
530
549
|
while True:
|
|
531
|
-
if gitdir != cwd and
|
|
550
|
+
if gitdir != cwd and gitdir.is_relative_to(cwd):
|
|
532
551
|
return cwd
|
|
533
552
|
parent = cwd.parent
|
|
534
553
|
if parent == cwd:
|
|
@@ -567,8 +586,13 @@ class Git(object):
|
|
|
567
586
|
|
|
568
587
|
def committish_exists(self, committish):
|
|
569
588
|
"""Return whether the committish exists in the repository."""
|
|
589
|
+
# Read the repository before the try. The bare except below would
|
|
590
|
+
# otherwise swallow the no-repository error and answer False, which
|
|
591
|
+
# says the committish is absent rather than that there is nothing to
|
|
592
|
+
# look in.
|
|
593
|
+
repo = self._repo
|
|
570
594
|
try:
|
|
571
|
-
|
|
595
|
+
repo.revparse_single(committish)
|
|
572
596
|
return True
|
|
573
597
|
except:
|
|
574
598
|
return False
|
|
@@ -770,16 +794,22 @@ class Git(object):
|
|
|
770
794
|
result = branch.upstream.branch_name
|
|
771
795
|
return result
|
|
772
796
|
|
|
773
|
-
def clone(self, url, ssh_id, path=None, bare=False):
|
|
774
|
-
"""Clone a
|
|
797
|
+
def clone(self, url, ssh_id=None, path=None, bare=False):
|
|
798
|
+
"""Clone a repository at the given url, making a bare clone if specified.
|
|
799
|
+
Authenticate with the ssh ID if one is given. A clone runs before there
|
|
800
|
+
is a repository config to read one from, so the caller supplies it.
|
|
801
|
+
Without an ssh ID we offer no key, which suits a local or an anonymous
|
|
802
|
+
url. An ssh url instead fails to authenticate.
|
|
803
|
+
|
|
804
|
+
"""
|
|
775
805
|
parsed_url = urllib.parse.urlparse(url)
|
|
776
806
|
url_path = Path(parsed_url.path).resolve()
|
|
777
807
|
url_name = url_path.name
|
|
778
808
|
target_path = path if path else str(Path.cwd() / url_name)
|
|
779
809
|
|
|
780
810
|
callbacks = Git.RemoteCallbacks(ssh_id)
|
|
781
|
-
self.
|
|
782
|
-
self._config = self.Config(self, self.
|
|
811
|
+
self._repository = pygit2.clone_repository(url, target_path, bare, callbacks=callbacks)
|
|
812
|
+
self._config = self.Config(self, self._repository.config)
|
|
783
813
|
|
|
784
814
|
return str(Path(target_path).resolve())
|
|
785
815
|
|
|
@@ -835,18 +865,18 @@ class Git(object):
|
|
|
835
865
|
self._repo.branches.delete(branch_name)
|
|
836
866
|
|
|
837
867
|
def remote_branch_exists(self, branch_name, remote):
|
|
838
|
-
"""Return
|
|
868
|
+
"""Return whether the given branch exists on the given remote."""
|
|
839
869
|
refname = self.branch_name_to_refname(branch_name)
|
|
840
870
|
# FIXME: This is likely to be something like
|
|
841
871
|
# refs/remotes/<remote>/<branch_name> but that is not what it's called
|
|
842
|
-
# in the
|
|
872
|
+
# in the list_heads call. There doesn't seem to be a way to get the
|
|
843
873
|
# (local) name of the branch on the remote side. Assume for now that
|
|
844
874
|
# it's the same as our local name.
|
|
845
875
|
remote_refname = self.get_remote_push_refname(refname, remote)
|
|
846
|
-
for item in self._repo.remotes[remote].
|
|
876
|
+
for item in self._repo.remotes[remote].list_heads(
|
|
847
877
|
callbacks=Git.LsRemotesCallbacks(self.get_ssh_id())
|
|
848
878
|
):
|
|
849
|
-
if not item
|
|
879
|
+
if not item.local and item.name == refname:
|
|
850
880
|
return True
|
|
851
881
|
return False
|
|
852
882
|
|