Skip to content

Commit aeea1c4

Browse files
codexByron
authored andcommitted
feat(gix): resolve supported committer identities through Gix
Use `Repository.committer()` for `GIT_COMMITTER_IDENT` and format the returned native signature. Keep unsupported normalization, empty identities, and per-command identity overrides on the CLI. GixPython 0.1.0 cannot pass an operation-specific environment to its identity resolver (`GIX-21`); Python must not fill in missing fields or manufacture the resolved identity. A supported process/config identity avoids one CLI invocation. An overridden query deliberately retains one CLI invocation (the previous Python overlay incorrectly counted it as zero). Native query parity uses a fixed process identity, and regressions compare partial and complete overrides with Git. The reference-format query also retains its documented `GIX-1` fallback. Validation: backend and commit tests passed with Gix first (65 passed in 10.42s), then CLI (37 passed, 1 skipped in 10.97s). Ruff lint/format and `git diff --check` passed.
1 parent 06d1e38 commit aeea1c4

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

‎git/_backend.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,26 @@ def _signature(env: Dict[str, Any], role: str) -> Any:
619619
return _checked_signature(gix.Signature(name, email, int(seconds), offset))
620620

621621

622+
def _committer(repo: Any, env: Dict[str, Any]) -> Any:
623+
for field in ("NAME", "EMAIL", "DATE"):
624+
key = "GIT_COMMITTER_" + field
625+
if key in env and env[key] != os.environ.get(key):
626+
raise _Unsupported("per-command committer identity (GIX-21)")
627+
if os.environ.get(key) == "":
628+
raise _Unsupported("empty identity")
629+
signature = repo.committer()
630+
if signature is None:
631+
raise _Unsupported("identity resolution")
632+
return _checked_signature(signature)
633+
634+
635+
def _var(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
636+
if args != ["GIT_COMMITTER_IDENT"]:
637+
raise _Unsupported("Git variable")
638+
signature = _committer(repo, kwargs.get("env", {}))
639+
return signature.name + b" <" + signature.email + b"> " + _date(signature) + b"\n"
640+
641+
622642
def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
623643
if len(args) < 2 or args[0] != "--no-gpg-sign" or args[2::2] != ["-p"] * len(args[2::2]):
624644
raise _Unsupported("commit options")
@@ -658,6 +678,7 @@ def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
658678
"read_tree": _read_tree,
659679
"commit_tree": _commit_tree,
660680
"reflog": _reflog,
681+
"var": _var,
661682
"config": _config,
662683
"worktree": _worktree,
663684
}

‎test/test_gix_backend.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
7575
assert cli.call_count == 3
7676

7777

78+
def test_native_command_queries_match_cli(repo, monkeypatch):
79+
Path(repo.working_dir, "file").write_bytes(b"content")
80+
repo.index.add(["file"])
81+
commit = repo.index.commit("initial", skip_hooks=True)
82+
monkeypatch.setenv("GIT_COMMITTER_NAME", "Example")
83+
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "example@example.invalid")
84+
monkeypatch.setenv("GIT_COMMITTER_DATE", "1100000000 -0430")
85+
with repo.config_writer() as writer:
86+
writer.set_value("test", "value", "configured")
87+
queries = [
88+
("rev_parse", "--verify", "--end-of-options", "HEAD"),
89+
("rev_parse", "--path-format=absolute", "--git-common-dir"),
90+
("rev_parse", "--is-bare-repository"),
91+
("rev_parse", "--show-object-format"),
92+
("rev_parse", "--show-toplevel"),
93+
("ls_tree", "-z", "--full-tree", commit.tree.hexsha),
94+
("symbolic_ref", "--quiet", "--no-recurse", "--", "HEAD"),
95+
("for_each_ref", "--format=%(refname)", "--", "refs/heads"),
96+
("config", "--get", "test.value"),
97+
("worktree", "list", "--porcelain", "-z"),
98+
("merge_base", "--", commit.hexsha, commit.hexsha),
99+
("ls_files", "--stage", "-v", "-z", "--full-name"),
100+
("update_index", "--show-index-version"),
101+
("reflog", "exists", "--", "HEAD"),
102+
("var", "GIT_COMMITTER_IDENT"),
103+
]
104+
for method, *args in queries:
105+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call: " + method)):
106+
native = repo.git._call_process_safe(method, *args, stdout_as_string=False)
107+
with patch.object(_backend, "gix", None):
108+
assert native == repo.git._call_process_safe(method, *args, stdout_as_string=False)
109+
110+
111+
@pytest.mark.parametrize("fields", [("NAME",), ("EMAIL",), ("DATE",), ("NAME", "EMAIL", "DATE")])
112+
def test_per_command_committer_identity_uses_cli(repo, fields, monkeypatch):
113+
monkeypatch.setenv("GIT_COMMITTER_NAME", "Process")
114+
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "process@example.invalid")
115+
monkeypatch.setenv("GIT_COMMITTER_DATE", "1000000000 +0000")
116+
values = {"NAME": "Override", "EMAIL": "override@example.invalid", "DATE": "1100000000 -0430"}
117+
env = {"GIT_COMMITTER_" + field: values[field] for field in fields}
118+
with patch.object(_backend, "gix", None):
119+
expected = repo.git._call_process_safe("var", "GIT_COMMITTER_IDENT", env=env)
120+
with patch.object(Git, "execute", autospec=True, side_effect=Git.execute) as cli:
121+
assert repo.git._call_process_safe("var", "GIT_COMMITTER_IDENT", env=env) == expected
122+
assert cli.call_count == 1
123+
124+
78125
def test_a_native_write_failure_is_not_retried(repo, monkeypatch):
79126
attempts = []
80127

0 commit comments

Comments
 (0)