Skip to content

Commit 5cc196a

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; Python must not fill missing fields or manufacture a resolved identity. Gix supports in-memory overrides for `committer.name`, `committer.email`, and `gitoxide.commit.committerDate`. Applying them to the retained repository would expose operation-specific identity to other threads. GixPython 0.1.0 binds no cheap independent repository clone: `copy.copy` and `copy.deepcopy` raise `TypeError`, while the binding's internal Rust `RepoHandle::clone()` shares its `Arc` state. Upstream should bind an independent native clone, with isolated configuration and native identity resolution (`GIX-21`). Reopening would isolate state but repeats repository setup. Keep the existing fallback until this is available; do not change the process environment or the shared snapshot. A supported process/config identity avoids one CLI invocation. A command override retains one. Native query parity uses a fixed process identity, and regressions compare partial and complete overrides with Git. The reference format query retains its repository-extension validation fallback (`GIX-1`). Original validation: backend and commit tests passed with Gix first (65 passed in 10.42s), then CLI (37 passed, 1 skipped in 10.97s), plus Ruff lint/format and `git diff --check`. This follow-up only clarifies the commit message; direct GixPython 0.1.0 probes verified native snapshot overrides and the absent copy API.
1 parent 5d2344a commit 5cc196a

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
@@ -628,6 +628,26 @@ def _signature(env: Dict[str, Any], role: str) -> Any:
628628
return _checked_signature(gix.Signature(name, email, int(seconds), offset))
629629

630630

631+
def _committer(repo: Any, env: Dict[str, Any]) -> Any:
632+
for field in ("NAME", "EMAIL", "DATE"):
633+
key = "GIT_COMMITTER_" + field
634+
if key in env and env[key] != os.environ.get(key):
635+
raise _Unsupported("per-command committer identity (GIX-21)")
636+
if os.environ.get(key) == "":
637+
raise _Unsupported("empty identity")
638+
signature = repo.committer()
639+
if signature is None:
640+
raise _Unsupported("identity resolution")
641+
return _checked_signature(signature)
642+
643+
644+
def _var(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
645+
if args != ["GIT_COMMITTER_IDENT"]:
646+
raise _Unsupported("Git variable")
647+
signature = _committer(repo, kwargs.get("env", {}))
648+
return signature.name + b" <" + signature.email + b"> " + _date(signature) + b"\n"
649+
650+
631651
def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
632652
if len(args) < 2 or args[0] != "--no-gpg-sign" or args[2::2] != ["-p"] * len(args[2::2]):
633653
raise _Unsupported("commit options")
@@ -667,6 +687,7 @@ def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
667687
"read_tree": _read_tree,
668688
"commit_tree": _commit_tree,
669689
"reflog": _reflog,
690+
"var": _var,
670691
"config": _config,
671692
"worktree": _worktree,
672693
}

‎test/test_gix_backend.py‎

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

111111

112+
def test_native_command_queries_match_cli(repo, monkeypatch):
113+
Path(repo.working_dir, "file").write_bytes(b"content")
114+
repo.index.add(["file"])
115+
commit = repo.index.commit("initial", skip_hooks=True)
116+
monkeypatch.setenv("GIT_COMMITTER_NAME", "Example")
117+
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "example@example.invalid")
118+
monkeypatch.setenv("GIT_COMMITTER_DATE", "1100000000 -0430")
119+
with repo.config_writer() as writer:
120+
writer.set_value("test", "value", "configured")
121+
queries = [
122+
("rev_parse", "--verify", "--end-of-options", "HEAD"),
123+
("rev_parse", "--path-format=absolute", "--git-common-dir"),
124+
("rev_parse", "--is-bare-repository"),
125+
("rev_parse", "--show-object-format"),
126+
("rev_parse", "--show-toplevel"),
127+
("ls_tree", "-z", "--full-tree", commit.tree.hexsha),
128+
("symbolic_ref", "--quiet", "--no-recurse", "--", "HEAD"),
129+
("for_each_ref", "--format=%(refname)", "--", "refs/heads"),
130+
("config", "--get", "test.value"),
131+
("worktree", "list", "--porcelain", "-z"),
132+
("merge_base", "--", commit.hexsha, commit.hexsha),
133+
("ls_files", "--stage", "-v", "-z", "--full-name"),
134+
("update_index", "--show-index-version"),
135+
("reflog", "exists", "--", "HEAD"),
136+
("var", "GIT_COMMITTER_IDENT"),
137+
]
138+
for method, *args in queries:
139+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call: " + method)):
140+
native = repo.git._call_process_safe(method, *args, stdout_as_string=False)
141+
with patch.object(_backend, "gix", None):
142+
assert native == repo.git._call_process_safe(method, *args, stdout_as_string=False)
143+
144+
145+
@pytest.mark.parametrize("fields", [("NAME",), ("EMAIL",), ("DATE",), ("NAME", "EMAIL", "DATE")])
146+
def test_per_command_committer_identity_uses_cli(repo, fields, monkeypatch):
147+
monkeypatch.setenv("GIT_COMMITTER_NAME", "Process")
148+
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "process@example.invalid")
149+
monkeypatch.setenv("GIT_COMMITTER_DATE", "1000000000 +0000")
150+
values = {"NAME": "Override", "EMAIL": "override@example.invalid", "DATE": "1100000000 -0430"}
151+
env = {"GIT_COMMITTER_" + field: values[field] for field in fields}
152+
with patch.object(_backend, "gix", None):
153+
expected = repo.git._call_process_safe("var", "GIT_COMMITTER_IDENT", env=env)
154+
with patch.object(Git, "execute", autospec=True, side_effect=Git.execute) as cli:
155+
assert repo.git._call_process_safe("var", "GIT_COMMITTER_IDENT", env=env) == expected
156+
assert cli.call_count == 1
157+
158+
112159
def test_a_native_write_failure_is_not_retried(repo, monkeypatch):
113160
attempts = []
114161

0 commit comments

Comments
 (0)