Skip to content

Commit d281b78

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 ffea84b commit d281b78

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

634634

635+
def _committer(repo: Any, env: Dict[str, Any]) -> Any:
636+
for field in ("NAME", "EMAIL", "DATE"):
637+
key = "GIT_COMMITTER_" + field
638+
if key in env and env[key] != os.environ.get(key):
639+
raise _Unsupported("per-command committer identity (GIX-21)")
640+
if os.environ.get(key) == "":
641+
raise _Unsupported("empty identity")
642+
signature = repo.committer()
643+
if signature is None:
644+
raise _Unsupported("identity resolution")
645+
return _checked_signature(signature)
646+
647+
648+
def _var(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
649+
if args != ["GIT_COMMITTER_IDENT"]:
650+
raise _Unsupported("Git variable")
651+
signature = _committer(repo, kwargs.get("env", {}))
652+
return signature.name + b" <" + signature.email + b"> " + _date(signature) + b"\n"
653+
654+
635655
def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
636656
if len(args) < 2 or args[0] != "--no-gpg-sign" or args[2::2] != ["-p"] * len(args[2::2]):
637657
raise _Unsupported("commit options")
@@ -671,6 +691,7 @@ def _commit_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
671691
"read_tree": _read_tree,
672692
"commit_tree": _commit_tree,
673693
"reflog": _reflog,
694+
"var": _var,
674695
"config": _config,
675696
"worktree": _worktree,
676697
}

‎test/test_gix_backend.py‎

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

112112

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

0 commit comments

Comments
 (0)