Skip to content

Commit 8ef249d

Browse files
blhsingclaude
andcommitted
feat: opt-in git shim via pythongit[git] extra, plus MIT LICENSE
The `git` console-script used to be auto-installed alongside `pygit`, which silently shadowed system git in any venv that had pythongit installed. This turns the drop-in behavior into a deliberate per-environment choice. Three install paths now exist; only the explicit ones add `git`: pip install pythongit -> only `pygit` pip install "pythongit[git]" -> `pygit` and `git` (via shim pkg) pygit install-git-shim -> add `git` after-the-fact The `[git]` extra pulls in a tiny companion distribution `pythongit-git-shim` (this repo, `pythongit-git-shim/` sub-tree) whose only role is to register the `git` console-script entry point. Uninstall the shim package with pip to cleanly remove the `git` command, or use `pygit uninstall-git-shim` for the post-install path. Both paths warn when a different `git` would still resolve first on PATH, and refuse to overwrite an unrelated file. Adds: - LICENSE (MIT) - pythongit/cli.py: `install-git-shim` / `uninstall-git-shim` commands - tests/unit_shim.py: 6 tests for the post-install path - pythongit-git-shim/: companion package with its own pyproject + LICENSE - pyproject.toml: `[project.optional-dependencies] git` entry, `license = { file = "LICENSE" }` - CI: builds and smoke-tests all three install paths Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eef288e commit 8ef249d

11 files changed

Lines changed: 398 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,51 @@ jobs:
7474
pygit --version
7575
pygit help | head -5
7676
77-
- name: Verify `git` drop-in entry point resolves to pythongit
77+
- name: Verify `git` is NOT installed by default
7878
shell: bash
7979
run: |
80-
# In the venv used by setup-python, our `git` shim lives in the
81-
# Scripts/bin dir. We invoke by absolute path so the system git on
82-
# PATH doesn't shadow it.
80+
scripts_dir=$(python -c "import sysconfig; print(sysconfig.get_path('scripts'))")
8381
if [[ "${{ runner.os }}" == "Windows" ]]; then
84-
"$(python -c 'import sys, os; print(os.path.join(os.path.dirname(sys.executable), "Scripts", "git.exe"))')" --version
82+
[ ! -f "$scripts_dir/git.exe" ] || (echo "unexpected git.exe in $scripts_dir"; exit 1)
8583
else
86-
"$(python -c 'import sys, os; print(os.path.join(os.path.dirname(sys.executable), "git"))')" --version
84+
[ ! -f "$scripts_dir/git" ] || (echo "unexpected git in $scripts_dir"; exit 1)
8785
fi
8886
87+
- name: Verify opt-in via `pygit install-git-shim`
88+
shell: bash
89+
run: |
90+
pygit install-git-shim
91+
scripts_dir=$(python -c "import sysconfig; print(sysconfig.get_path('scripts'))")
92+
if [[ "${{ runner.os }}" == "Windows" ]]; then
93+
shim="$scripts_dir/git.exe"
94+
else
95+
shim="$scripts_dir/git"
96+
fi
97+
"$shim" --version | grep -q "pygit version"
98+
"$shim" help | grep -q "init"
99+
pygit uninstall-git-shim
100+
[ ! -f "$shim" ] || (echo "uninstall left $shim behind"; exit 1)
101+
102+
- name: Verify opt-in via `pip install "pythongit[git]"` extra
103+
shell: bash
104+
run: |
105+
# Install the sibling shim package from this repo, then re-install
106+
# pythongit with the [git] extra. The extra references
107+
# pythongit-git-shim, which should now resolve locally.
108+
python -m pip install ./pythongit-git-shim
109+
scripts_dir=$(python -c "import sysconfig; print(sysconfig.get_path('scripts'))")
110+
if [[ "${{ runner.os }}" == "Windows" ]]; then
111+
shim="$scripts_dir/git.exe"
112+
else
113+
shim="$scripts_dir/git"
114+
fi
115+
[ -f "$shim" ] || (echo "git shim not installed by the shim wheel"; exit 1)
116+
"$shim" --version | grep -q "pygit version"
117+
# Clean up so the test step (which does not want this extra installed)
118+
# ends back in the default state.
119+
python -m pip uninstall -y pythongit-git-shim
120+
[ ! -f "$shim" ] || (echo "pip uninstall left $shim behind"; exit 1)
121+
89122
build:
90123
name: build wheel + sdist
91124
runs-on: ubuntu-latest
@@ -100,18 +133,41 @@ jobs:
100133
- name: Install build tools
101134
run: python -m pip install --upgrade build twine
102135

103-
- name: Build
136+
- name: Build pythongit (main package)
104137
run: python -m build
105138

106-
- name: twine check
139+
- name: Build pythongit-git-shim (companion package)
140+
run: python -m build pythongit-git-shim --outdir dist
141+
142+
- name: twine check both distributions
107143
run: python -m twine check dist/*
108144

109-
- name: Smoke-install the built wheel
145+
- name: Smoke-install — default (no `git` shim)
110146
run: |
111147
python -m venv /tmp/install-venv
112-
/tmp/install-venv/bin/pip install dist/*.whl
148+
/tmp/install-venv/bin/pip install dist/pythongit-*.whl
113149
/tmp/install-venv/bin/pygit --version
114-
/tmp/install-venv/bin/git --version
150+
# `git` must NOT be installed by default.
151+
[ ! -f /tmp/install-venv/bin/git ] || (echo "unexpected git in /tmp/install-venv/bin"; exit 1)
152+
153+
- name: Smoke-install — `pythongit[git]` extra
154+
run: |
155+
python -m venv /tmp/install-extra-venv
156+
# Install both wheels so the extra resolves against local artifacts.
157+
/tmp/install-extra-venv/bin/pip install dist/pythongit_git_shim-*.whl dist/pythongit-*.whl
158+
/tmp/install-extra-venv/bin/pygit --version
159+
/tmp/install-extra-venv/bin/git --version | grep -q "pygit version"
160+
/tmp/install-extra-venv/bin/pip uninstall -y pythongit-git-shim
161+
[ ! -f /tmp/install-extra-venv/bin/git ] || (echo "uninstall failed"; exit 1)
162+
163+
- name: Smoke-install — `pygit install-git-shim` post-install path
164+
run: |
165+
python -m venv /tmp/install-postvenv
166+
/tmp/install-postvenv/bin/pip install dist/pythongit-*.whl
167+
/tmp/install-postvenv/bin/pygit install-git-shim
168+
/tmp/install-postvenv/bin/git --version | grep -q "pygit version"
169+
/tmp/install-postvenv/bin/pygit uninstall-git-shim
170+
[ ! -f /tmp/install-postvenv/bin/git ] || (echo "uninstall failed"; exit 1)
115171
116172
- name: Upload distribution artifacts
117173
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ build/
33
dist/
44
*.egg-info/
55
*.egg
6+
pythongit-git-shim/build/
7+
pythongit-git-shim/dist/
8+
pythongit-git-shim/*.egg-info/
69

710
# Python
811
__pycache__/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 pythongit contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,50 @@ implements.
5454
pip install pythongit
5555
```
5656

57-
This installs **two console scripts**:
57+
By default this installs **one console script**: `pygit`. The system `git`
58+
binary on your PATH is **not** shadowed unless you explicitly opt in.
5859

59-
| Script | Purpose |
60-
|---------|-----------------------------------------------------------|
61-
| `pygit` | Unambiguous name; always invokes pythongit |
62-
| `git` | Drop-in name; shadows real `git` only if it comes earlier on PATH |
60+
### Opt-in `git` drop-in
6361

64-
If a real `git` binary is already on PATH and earlier than the venv's `Scripts/`
65-
or `bin/` directory, your shell will resolve `git` to the real one. To force
66-
the pythongit version, either use `pygit`, put the venv earlier on PATH, or run
67-
`python -m pythongit ...`.
62+
The `git` command name is **not** installed by default. You can opt in two ways:
6863

69-
You can also run from a checkout without installing:
64+
**1. The standard extras syntax — recommended:**
65+
66+
```bash
67+
pip install "pythongit[git]"
68+
```
69+
70+
This pulls in the tiny companion package `pythongit-git-shim`, which exists
71+
only to register a `git` console-script. Uninstall it with
72+
`pip uninstall pythongit-git-shim` to remove the `git` command without
73+
touching the rest of pythongit.
74+
75+
**2. After-the-fact, without reinstalling:**
76+
77+
```bash
78+
pygit install-git-shim
79+
```
80+
81+
This copies `pygit` to a sibling `git` (or `git.exe` on Windows) in the same
82+
scripts directory. Reverse with `pygit uninstall-git-shim`. Useful when you
83+
already have pythongit installed and don't want to touch the pip metadata.
84+
85+
Whichever way you choose, whether `git` resolves to pythongit depends on PATH
86+
order — both commands warn if a different `git` is earlier on PATH.
87+
88+
You can also run pythongit from a checkout without installing:
7089

7190
```bash
7291
python -m pythongit <command> [args...]
7392
```
7493

94+
### Why is the `git` name opt-in?
95+
96+
Silently shadowing `git` on every install is a footgun: scripts that shell
97+
out to `git` start invoking pythongit instead the next time you
98+
`pip install pythongit` into a venv, without warning. Making it opt-in turns
99+
it into a deliberate choice you make per-environment.
100+
75101
## Tutorial
76102

77103
```bash

pyproject.toml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "pythongit"
77
version = "0.1.0"
8-
description = "Pure-Python reimplementation of git. Drop-in replacement that exposes both `pygit` and `git` console scripts."
8+
description = "Pure-Python reimplementation of git. Installs `pygit`; opt in to a drop-in `git` shim via `pygit install-git-shim`."
99
readme = "README.md"
1010
requires-python = ">=3.9"
1111
authors = [{ name = "pythongit" }]
12-
license = { text = "MIT" }
12+
license = { file = "LICENSE" }
1313
keywords = ["git", "vcs", "version-control", "scm"]
1414
classifiers = [
1515
"Development Status :: 4 - Beta",
@@ -33,13 +33,17 @@ Homepage = "https://github.com/example/pythongit"
3333
Repository = "https://github.com/example/pythongit"
3434

3535
[project.scripts]
36-
# Primary CLI name.
36+
# The only auto-installed entry point. The `git` drop-in is opt-in via
37+
# `pygit install-git-shim` (see README), so installing pythongit never
38+
# shadows a system `git` by accident.
3739
pygit = "pythongit.cli:main"
38-
# Drop-in name. When this package is installed in an environment without a
39-
# real `git` binary on PATH, calling `git ...` invokes pythongit instead.
40-
git = "pythongit.cli:main"
4140

4241
[project.optional-dependencies]
42+
# Opt in to the drop-in `git` command via `pip install pythongit[git]`. This
43+
# pulls in pythongit-git-shim, whose only role is to register a `git`
44+
# console-script that calls pythongit.cli:main. Uninstall
45+
# `pythongit-git-shim` to remove the shim without uninstalling pythongit.
46+
git = ["pythongit-git-shim==0.1.0"]
4347
test = ["pytest>=7"]
4448

4549
[tool.setuptools.packages.find]

pythongit-git-shim/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 pythongit contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

pythongit-git-shim/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# pythongit-git-shim
2+
3+
A tiny companion package to [pythongit](https://github.com/blhsing/pythongit)
4+
whose only purpose is to install a `git` console-script that calls
5+
`pythongit.cli:main`.
6+
7+
Install via the `pythongit[git]` extra:
8+
9+
```bash
10+
pip install "pythongit[git]"
11+
```
12+
13+
That pulls in `pythongit` and this shim, giving you both `pygit` and `git`
14+
commands. Uninstalling this shim (`pip uninstall pythongit-git-shim`) cleanly
15+
removes the `git` console-script and leaves `pygit` working.
16+
17+
The point of having this as a separate distribution is that
18+
`[project.scripts]` in `pyproject.toml` can't be gated by extras — every
19+
declared entry point gets installed unconditionally. By moving the `git`
20+
entry point into a separate distribution, we make the drop-in behavior an
21+
opt-in choice rather than something that silently happens on every
22+
`pip install pythongit`.
23+
24+
See pythongit's main README for usage.

pythongit-git-shim/pyproject.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[build-system]
2+
requires = ["setuptools>=61"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "pythongit-git-shim"
7+
version = "0.1.0"
8+
description = "Drop-in `git` console-script that delegates to pythongit. Install via `pip install pythongit[git]`."
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
authors = [{ name = "pythongit" }]
12+
license = { file = "LICENSE" }
13+
dependencies = ["pythongit==0.1.0"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Environment :: Console",
17+
"License :: OSI Approved :: MIT License",
18+
"Operating System :: OS Independent",
19+
"Programming Language :: Python :: 3",
20+
"Topic :: Software Development :: Version Control :: Git",
21+
]
22+
23+
[project.scripts]
24+
# This is the whole purpose of the package: install a `git` console-script
25+
# that calls pythongit's CLI dispatcher. Anyone who doesn't want their `git`
26+
# command shadowed should simply not install this package.
27+
git = "pythongit.cli:main"
28+
29+
[tool.setuptools.packages.find]
30+
include = ["pythongit_git_shim*"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""pythongit_git_shim is intentionally empty.
2+
3+
This package exists only to declare a `git` console-script entry point that
4+
delegates to `pythongit.cli:main`. It carries no runtime code — installing it
5+
is the act of opting in to the drop-in `git` name. See pyproject.toml.
6+
"""

0 commit comments

Comments
 (0)