Skip to content

Commit a8af8ab

Browse files
codexByron
authored andcommitted
refactor(gix): centralize retained repository access on Repo
Native operations already retain a `gix.Repository`, but the choice to reuse or recreate it was hidden inside backend dispatch. Route bound access through `Repo._get_gix_repository()` and provide `recreate=True` as the control point for a future configurable policy. Preserve individual command options and the existing refresh, locking, pickling and resource-release behavior. Document retained native caches as an intentional deviation from a fresh Git process per command. Configuration queries still use a separate fresh handle so the execution handle's safety overrides do not affect queried values. CLI-call reduction: 0. The fixed probe of ten native metadata queries starts 0 CLI processes before and after this ownership refactor. Explicit recreation also starts none; later commits can use this accessor for their conversions. Validation: Gix first, 304 tests and 14 subtests passed (3 skipped), then CLI, 264 tests and 14 subtests passed (4 skipped). Four targeted Gix cases also passed after checking preservation of a separate bound command's options. Ruff lint/format, mypy and basedpyright passed.
1 parent a371116 commit a8af8ab

4 files changed

Lines changed: 57 additions & 6 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,25 @@ count: raw `repo.git` calls and subprocesses started by Git are not all counted.
284284

285285
### Retain native repository state (implemented)
286286

287-
Each `git.Repo` owns a native `gix.Repository`. Managed operations reuse it;
287+
Each `git.Repo` owns a native `gix.Repository`. Managed operations access it through
288+
`Repo._get_gix_repository()`, which reuses the instance by default.
289+
`Repo._get_gix_repository(recreate=True)` replaces it explicitly; this method is
290+
the control point for a future configurable reuse policy. Managed operations reuse it;
288291
`close()` releases it, and a later operation can reopen it. Pickling excludes
289292
native resources and restores the command's weak owner reference. Gix provides
290293
thread-safe handle access and automatic index/ODB snapshot refresh. A per-Repo
291294
lock serializes handle refresh, without serializing read operations.
292295

296+
Retaining native state is an intentional deviation from Git's fresh process per
297+
command. Configuration/environment and observed CLI changes trigger best-effort
298+
refresh, but this is not a promise that every external filesystem change is
299+
immediately visible in every native cache. Explicit recreation or closing and
300+
reopening the `Repo` provides fresh native state. Configuration queries currently
301+
use a separate fresh handle because the retained execution handle overrides
302+
hooks, fsmonitor and automatic maintenance; those overrides must not leak into
303+
configuration results. The accessor itself removes no CLI calls: ten supported
304+
metadata queries launch zero processes before and after this refactor.
305+
293306
Configuration and storage metadata changes, environment changes and raw CLI
294307
launches cause `reload()` before reuse. Configuration queries use a separate
295308
fresh handle so synthetic safety settings never appear as user settings.
@@ -458,8 +471,10 @@ unclear capabilities here and retain the CLI path until Gix provides them.
458471
Decide whether to fall back before mutating anything. Read/preparation failures
459472
can use Git to preserve its public diagnostics. Once `_write()` begins, native
460473
errors become `GitCommandError` and must never trigger a second CLI mutation.
461-
Open a fresh native repository for each operation so CLI fallback cannot leave
462-
a cached native view stale. Close native iterators when partially consumed.
474+
Access native repository state through `Repo._get_gix_repository()` for bound
475+
commands. Reuse is the default, with explicit recreation available; keep the
476+
snapshot deviation and refresh behavior above documented. Close native iterators
477+
when partially consumed.
463478

464479
Add each conversion with a no-subprocess check and a Git parity check where
465480
practical, in its own commit. Update this ledger when a limitation changes;

‎git/_backend.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ def discover_repository(path: str, environment: Dict[str, Any]) -> Any:
102102
def _repository(command: Any, env: Dict[str, Any], *, query_config: bool = False) -> Any:
103103
owner = command._repo() if command._repo is not None else None
104104
if owner is not None:
105-
with owner._gix_lock:
106-
return _open_repository(command, env, query_config=query_config)
105+
return owner._get_gix_repository(command=command, env=env, query_config=query_config)
107106
return _open_repository(command, env, query_config=query_config)
108107

109108

‎git/repo/base.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,28 @@ def __init__(
475475
)
476476
self.odb = odbt(rootpath)
477477

478+
def _get_gix_repository(
479+
self,
480+
*,
481+
command: Optional[Git] = None,
482+
env: Optional[Dict[str, Any]] = None,
483+
query_config: bool = False,
484+
recreate: bool = False,
485+
) -> Any:
486+
"""Access native state, reusing the repository unless recreation is requested.
487+
488+
This method owns the reuse policy so it can later become configurable.
489+
Configuration queries currently open a separate handle without the execution
490+
restrictions applied to the retained handle. Other operations retain the
491+
existing best-effort refresh of configuration, environment and CLI changes.
492+
"""
493+
with self._gix_lock:
494+
if recreate:
495+
self._gix_repository = self._gix_state = None
496+
return _backend._open_repository(
497+
command if command is not None else self.git, env or {}, query_config=query_config
498+
)
499+
478500
def __getstate__(self) -> Dict[str, Any]:
479501
return {
480502
key: value

‎test/test_gix_backend.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_native_worktree_inventory_includes_bare_main(repo, tmp_path):
5959

6060

6161
def test_native_repository_lifetime_and_refresh(repo, tmp_path):
62-
handle = repo._gix_repository
62+
handle = repo._get_gix_repository()
6363
assert isinstance(handle, gix.Repository)
6464
with patch.object(gix, "open_opts", side_effect=AssertionError("unexpected reopen")):
6565
assert repo.git._call_process_safe("rev_parse", "--show-object-format") == repo.object_format
@@ -91,6 +91,14 @@ def test_native_repository_lifetime_and_refresh(repo, tmp_path):
9191
include.write_text("[test]\nvalue = second\n")
9292
assert _backend._repository(repo.git, {}).config_snapshot().string("test.value") == b"second"
9393

94+
count = _backend.statistics().get(("Git.execute", "CLI process"), 0)
95+
with patch.object(gix, "open_opts", wraps=gix.open_opts) as opened:
96+
replacement = repo._get_gix_repository(recreate=True)
97+
assert replacement is not handle
98+
assert repo._get_gix_repository() is replacement
99+
opened.assert_called_once()
100+
assert _backend.statistics().get(("Git.execute", "CLI process"), 0) == count
101+
94102
with pickle.loads(pickle.dumps(repo)) as restored:
95103
assert restored._gix_repository is None
96104
assert restored.commit().hexsha == repo.commit().hexsha
@@ -212,6 +220,13 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
212220
assert repo.git._call_process_safe("rev_parse", "--show-ref-format") == "fallback"
213221
assert cli.call_count == 3
214222

223+
command = Git(repo.working_dir)
224+
command._repo = repo.git._repo
225+
command._environment.update(repo.git.environment())
226+
command._persistent_git_options = ["--no-replace-objects"]
227+
with pytest.raises(_backend._Unsupported, match="global command options"):
228+
_backend._repository(command, {})
229+
215230

216231
def test_compatibility_object_format_uses_cli_before_writing(repo):
217232
repo.git.config("core.repositoryFormatVersion", "1")

0 commit comments

Comments
 (0)