Skip to content

Commit 438e308

Browse files
committed
Enforce Git 2.54 parity registry
1 parent e458c3c commit 438e308

7 files changed

Lines changed: 334 additions & 81 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,67 @@ jobs:
119119
python -m pip uninstall -y pure-python-git-shim
120120
[ ! -f "$shim" ] || (echo "pip uninstall left $shim behind"; exit 1)
121121
122+
git-254-parity:
123+
name: C Git 2.54.0 parity
124+
runs-on: ubuntu-latest
125+
env:
126+
GIT_254_URL: https://github.com/git/git/archive/refs/tags/v2.54.0.tar.gz
127+
GIT_254_SHA256: 7b01a23c44c9ccfca2e3ad9daf8cbdd4d4caaaa6b5181e77e16e60c6ae5f772a
128+
GIT_254_PREFIX: ${{ github.workspace }}/.cache/git-2.54.0
129+
steps:
130+
- name: Checkout
131+
uses: actions/checkout@v4
132+
133+
- name: Set up Python 3.13
134+
uses: actions/setup-python@v5
135+
with:
136+
python-version: "3.13"
137+
cache: pip
138+
139+
- name: Cache C Git 2.54.0 build
140+
id: cache-git-254
141+
uses: actions/cache@v4
142+
with:
143+
path: .cache/git-2.54.0
144+
key: git-2.54.0-ubuntu-${{ env.GIT_254_SHA256 }}
145+
146+
- name: Install C Git build dependencies
147+
if: steps.cache-git-254.outputs.cache-hit != 'true'
148+
run: |
149+
sudo apt-get update
150+
sudo apt-get install -y build-essential gettext libcurl4-gnutls-dev libexpat1-dev libssl-dev zlib1g-dev
151+
152+
- name: Build C Git 2.54.0 oracle
153+
if: steps.cache-git-254.outputs.cache-hit != 'true'
154+
shell: bash
155+
run: |
156+
curl -L "$GIT_254_URL" -o /tmp/git-2.54.0.tar.gz
157+
echo "$GIT_254_SHA256 /tmp/git-2.54.0.tar.gz" | sha256sum -c -
158+
rm -rf /tmp/git-2.54.0-src
159+
mkdir -p /tmp/git-2.54.0-src "$GIT_254_PREFIX"
160+
tar -xzf /tmp/git-2.54.0.tar.gz -C /tmp/git-2.54.0-src --strip-components=1
161+
make -C /tmp/git-2.54.0-src -j"$(nproc)" prefix="$GIT_254_PREFIX" NO_TCLTK=YesPlease all install
162+
163+
- name: Verify oracle version
164+
run: |
165+
"$GIT_254_PREFIX/bin/git" --version | grep -q "git version 2.54.0"
166+
167+
- name: Install package + test deps
168+
run: |
169+
python -m pip install --upgrade pip
170+
python -m pip install -e ".[test]"
171+
172+
- name: Configure oracle identity
173+
run: |
174+
"$GIT_254_PREFIX/bin/git" config --global user.name "ci"
175+
"$GIT_254_PREFIX/bin/git" config --global user.email "ci@example.invalid"
176+
"$GIT_254_PREFIX/bin/git" config --global init.defaultBranch main
177+
178+
- name: Run required parity tests
179+
env:
180+
PYGIT_PARITY_GIT: ${{ env.GIT_254_PREFIX }}/bin/git
181+
run: python -m pytest -v --color=yes tests/git_parity --require-git-254-oracle
182+
122183
build:
123184
name: build wheel + sdist
124185
runs-on: ubuntu-latest

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,16 @@ pygit convert-object-format --object-format=sha1 ./sha256-copy ./sha1-copy
119119
The command surface covers git built-ins plus aliases and pythongit-specific
120120
helpers. CLI parity is tracked against C Git 2.54.0 from the upstream
121121
implementation source: `tools/git_cli_manifest.py` generates
122-
`tests/git_parity/manifest/git-2.54.0.json`, and optional behavior checks run
123-
when `PYGIT_PARITY_GIT` points at a `git version 2.54.0` binary. Selected
124-
highlights:
122+
`tests/git_parity/manifest/git-2.54.0.json`. A dedicated CI job builds a
123+
`git version 2.54.0` oracle and runs:
124+
125+
```bash
126+
PYGIT_PARITY_GIT=/path/to/git-2.54.0 \
127+
python -m pytest tests/git_parity --require-git-254-oracle
128+
```
129+
130+
Without `--require-git-254-oracle`, local runs skip behavior checks when the
131+
oracle binary is unavailable. Selected highlights:
125132

126133
**Plumbing.** `hash-object`, `cat-file`, `ls-tree`, `write-tree`, `read-tree`,
127134
`commit-tree`, `mktree`, `mktag`, `update-ref`, `symbolic-ref`, `rev-parse`,

pythongit/cli.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,18 @@ def cmd_uninstall_git_shim(argv: list[str]) -> int:
10811081
return 0
10821082

10831083

1084+
def cmd_version(argv: list[str]) -> int:
1085+
ap = argparse.ArgumentParser(prog="pygit version")
1086+
ap.add_argument("--build-options", action="store_true")
1087+
args = ap.parse_args(argv)
1088+
from . import __version__
1089+
_print(f"pygit version {__version__}")
1090+
if args.build_options:
1091+
_print("cpu: pure-python")
1092+
_print("sizeof-long: 8")
1093+
return 0
1094+
1095+
10841096
# ---------------------------------------------------------------------------
10851097
# stubs that record they are not yet implemented (Phase 2)
10861098

@@ -1142,6 +1154,7 @@ def _f(argv):
11421154
"fsck": cmd_fsck,
11431155
"gc": cmd_gc,
11441156
"help": cmd_help,
1157+
"version": cmd_version,
11451158
"install-git-shim": cmd_install_git_shim,
11461159
"uninstall-git-shim": cmd_uninstall_git_shim,
11471160
}
@@ -5089,8 +5102,10 @@ def _f(_argv):
50895102

50905103

50915104
def _register_phase8() -> None:
5105+
_COMMANDS["stage"] = cmd_add
50925106
_COMMANDS["init-db"] = cmd_init_db
50935107
_COMMANDS["annotate"] = cmd_annotate
5108+
_COMMANDS["pickaxe"] = cmd_blame
50945109
_COMMANDS["patch-id"] = cmd_patch_id
50955110
_COMMANDS["checkout-index"] = cmd_checkout_index
50965111
_COMMANDS["fmt-merge-msg"] = cmd_fmt_merge_msg
@@ -5099,9 +5114,14 @@ def _register_phase8() -> None:
50995114
_COMMANDS["upload-pack"] = cmd_upload_pack
51005115
_COMMANDS["receive-pack"] = cmd_receive_pack
51015116
_COMMANDS["upload-archive"] = cmd_upload_archive
5117+
_COMMANDS["upload-archive--writer"] = cmd_upload_archive
51025118
_COMMANDS["pack-redundant"] = cmd_pack_redundant
51035119
_COMMANDS["prune-packed"] = cmd_prune_packed
5120+
_COMMANDS["fsck-objects"] = cmd_fsck
51045121
_COMMANDS["merge-recursive"] = cmd_merge_recursive
5122+
_COMMANDS["merge-recursive-ours"] = cmd_merge_recursive
5123+
_COMMANDS["merge-recursive-theirs"] = cmd_merge_recursive
5124+
_COMMANDS["merge-subtree"] = cmd_merge_recursive
51055125
_COMMANDS["merge-ours"] = cmd_merge_ours
51065126
_COMMANDS["multi-pack-index"] = cmd_multi_pack_index
51075127
_COMMANDS["for-each-repo"] = cmd_for_each_repo
@@ -5113,7 +5133,9 @@ def _register_phase8() -> None:
51135133
_COMMANDS["replay"] = cmd_replay
51145134
_COMMANDS["backfill"] = cmd_backfill
51155135
_COMMANDS["convert-object-format"] = cmd_convert_object_format
5136+
_COMMANDS["submodule--helper"] = cmd_submodule_helper
51165137
_COMMANDS["submodule-helper"] = cmd_submodule_helper
5138+
_COMMANDS["checkout--worker"] = cmd_checkout_worker
51175139
_COMMANDS["checkout-worker"] = cmd_checkout_worker
51185140

51195141
# explicit out-of-scope stubs
@@ -5395,9 +5417,11 @@ def _cmd_svn(argv):
53955417
_COMMANDS["cvsserver"] = _cmd_cvs_bridge("cvsserver")
53965418
_COMMANDS["svn"] = _cmd_svn
53975419
_COMMANDS["credential-cache"] = _cmd_credential_cache
5420+
_COMMANDS["credential-cache--daemon"] = _cmd_credential_cache_daemon
53985421
_COMMANDS["credential-cache-daemon"] = _cmd_credential_cache_daemon
53995422
_COMMANDS["credential-store"] = _cmd_credential_store
54005423
_COMMANDS["fsmonitor"] = _cmd_fsmonitor
5424+
_COMMANDS["fsmonitor--daemon"] = _cmd_fsmonitor_daemon
54015425
_COMMANDS["fsmonitor-daemon"] = _cmd_fsmonitor_daemon
54025426
_COMMANDS["remote-helper"] = _cmd_mergetool_remote_helper
54035427
_COMMANDS["remote-ext"] = _cmd_remote_ext
@@ -5563,6 +5587,40 @@ def _cmd_repo(argv):
55635587
_register_phase8()
55645588

55655589

5590+
_PYGIT_EXTENSION_COMMANDS = frozenset({
5591+
"checkout-worker",
5592+
"convert-object-format",
5593+
"credential-cache-daemon",
5594+
"cvsexportcommit",
5595+
"cvsimport",
5596+
"cvsserver",
5597+
"daemon",
5598+
"fsmonitor",
5599+
"fsmonitor-daemon",
5600+
"gitk",
5601+
"gitweb",
5602+
"gui",
5603+
"http-backend",
5604+
"http-fetch",
5605+
"install-git-shim",
5606+
"instaweb",
5607+
"mergetool",
5608+
"remote-helper",
5609+
"request-pull",
5610+
"send-email",
5611+
"shell",
5612+
"submodule",
5613+
"submodule-helper",
5614+
"svn",
5615+
"uninstall-git-shim",
5616+
"url-parse",
5617+
})
5618+
5619+
5620+
def cgit_compatible_commands() -> set[str]:
5621+
return set(_COMMANDS) - set(_PYGIT_EXTENSION_COMMANDS)
5622+
5623+
55665624
_GLOBAL_FLAGS_IGNORED = {
55675625
"-p", "--paginate", "-P", "--no-pager",
55685626
"--no-replace-objects", "--no-lazy-fetch", "--no-optional-locks",
@@ -5701,10 +5759,12 @@ def main(argv: Optional[list[str]] = None) -> int:
57015759
return cmd_help([])
57025760
if argv[0] == _GLOBAL_EXIT:
57035761
return 0
5704-
if argv[0] in ("--version", "-v", "version"):
5762+
if argv[0] in ("--version", "-v"):
57055763
from . import __version__
57065764
_print(f"pygit version {__version__}")
57075765
return 0
5766+
if argv[0] == "version":
5767+
return cmd_version(argv[1:])
57085768
cmd = argv[0]
57095769
rest = argv[1:]
57105770
fn = _COMMANDS.get(cmd)

tests/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
sys.path.insert(0, str(ROOT))
1515

1616

17+
def pytest_addoption(parser):
18+
parser.addoption(
19+
"--require-git-254-oracle",
20+
action="store_true",
21+
help="fail Git parity tests unless PYGIT_PARITY_GIT is a git version 2.54.0 binary",
22+
)
23+
24+
1725
def _is_real_git(path: str) -> bool:
1826
"""A real git binary identifies itself as 'git version X.Y.Z'. Pythongit's
1927
`git` shim prints 'pygit version ...' for --version. Use this to
@@ -58,6 +66,31 @@ def real_git() -> str | None:
5866
return None
5967

6068

69+
@pytest.fixture
70+
def git_254_oracle(pytestconfig) -> str:
71+
git = os.environ.get("PYGIT_PARITY_GIT")
72+
require = pytestconfig.getoption("--require-git-254-oracle")
73+
if not git:
74+
msg = "set PYGIT_PARITY_GIT to a C Git 2.54.0 binary"
75+
if require:
76+
pytest.fail(msg)
77+
pytest.skip(msg)
78+
try:
79+
result = subprocess.run([git, "--version"], text=True, capture_output=True, timeout=10)
80+
except (OSError, subprocess.TimeoutExpired) as exc:
81+
msg = f"could not run PYGIT_PARITY_GIT={git!r}: {exc}"
82+
if require:
83+
pytest.fail(msg)
84+
pytest.skip(msg)
85+
version = (result.stdout + result.stderr).strip()
86+
if result.returncode != 0 or not version.startswith("git version 2.54.0"):
87+
msg = f"PYGIT_PARITY_GIT must be git version 2.54.0, got {version!r}"
88+
if require:
89+
pytest.fail(msg)
90+
pytest.skip(msg)
91+
return git
92+
93+
6194
@pytest.fixture
6295
def tmprepo(tmp_path: Path):
6396
"""Initialize a fresh repository and chdir into it. Yields (Repository, Path)."""

tests/git_parity/support.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
9+
ROOT = Path(__file__).resolve().parents[2]
10+
PYGIT = [sys.executable, "-m", "pythongit"]
11+
12+
13+
def pygit_cmd() -> list[str]:
14+
return list(PYGIT)
15+
16+
17+
def run_cmd(
18+
cmd: list[str],
19+
cwd: Path,
20+
*,
21+
env: dict[str, str] | None = None,
22+
stdin: str | None = None,
23+
timeout: int = 10,
24+
) -> subprocess.CompletedProcess[str]:
25+
merged_env = os.environ.copy()
26+
merged_env["PYTHONPATH"] = str(ROOT) + os.pathsep + merged_env.get("PYTHONPATH", "")
27+
if env:
28+
merged_env.update(env)
29+
return subprocess.run(
30+
cmd,
31+
cwd=cwd,
32+
env=merged_env,
33+
input=stdin,
34+
text=True,
35+
capture_output=True,
36+
timeout=timeout,
37+
)
38+
39+
40+
def init_with_pygit(path: Path) -> None:
41+
path.mkdir()
42+
result = run_cmd([*pygit_cmd(), "init", "-b", "main", "."], path)
43+
assert result.returncode == 0, result.stderr
44+
45+
46+
def init_repo_pair(tmp_path: Path) -> tuple[Path, Path]:
47+
oracle_repo = tmp_path / "oracle"
48+
actual_repo = tmp_path / "actual"
49+
init_with_pygit(oracle_repo)
50+
init_with_pygit(actual_repo)
51+
return oracle_repo, actual_repo
52+
53+
54+
def configure_pygit_identity(path: Path) -> None:
55+
assert run_cmd([*pygit_cmd(), "config", "user.name", "Parity"], path).returncode == 0
56+
assert run_cmd([*pygit_cmd(), "config", "user.email", "parity@example.com"], path).returncode == 0
57+
58+
59+
def assert_same_result(actual: subprocess.CompletedProcess[str], oracle: subprocess.CompletedProcess[str]) -> None:
60+
assert actual.returncode == oracle.returncode
61+
assert actual.stdout == oracle.stdout
62+
assert actual.stderr == oracle.stderr

0 commit comments

Comments
 (0)