Skip to content

Commit a6b50fd

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 fd9adbd commit a6b50fd

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
@@ -281,12 +281,25 @@ count: raw `repo.git` calls and subprocesses started by Git are not all counted.
281281

282282
### Retain native repository state (implemented)
283283

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

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

461476
Add each conversion with a no-subprocess check and a Git parity check where
462477
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
@@ -98,8 +98,7 @@ def discover_repository(path: str, environment: Dict[str, Any]) -> Any:
9898
def _repository(command: Any, env: Dict[str, Any], *, query_config: bool = False) -> Any:
9999
owner = command._repo() if command._repo is not None else None
100100
if owner is not None:
101-
with owner._gix_lock:
102-
return _open_repository(command, env, query_config=query_config)
101+
return owner._get_gix_repository(command=command, env=env, query_config=query_config)
103102
return _open_repository(command, env, query_config=query_config)
104103

105104

‎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
@@ -58,7 +58,7 @@ def test_native_worktree_inventory_includes_bare_main(repo, tmp_path):
5858

5959

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

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

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

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

0 commit comments

Comments
 (0)