FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(repo): retain checkout context for separate Git directories · gitpython-developers/GitPython@46c4bed · GitHub

Repository navigation

Commit 46c4bed

Browse files
authored andcommitted
fix(repo): retain checkout context for separate Git directories
Mistral Vibe 3.0.1's two separate-directory worktree tests fail when `Repo.init(..., separate_git_dir=...)` returns the administrative directory's parent as `working_tree_dir`. Index staging then cannot find the files in the requested checkout. Released GitPython 3.2.1 passes the same tests. Distinguish a checkout's `.git` file from an arbitrary gitfile operand when opening a discovery candidate. Reopen the canonical checkout gitfile through `gix.open_opts()` and use its native `Repository.workdir()` result; reopening only its administrative target discards the supplied checkout context. Keep the existing canonical administrative reopen for standalone arbitrary-gitfile helpers and their GIX-14 workaround. In the CLI path, ask Git for the top-level directory from the supplied checkout instead of inferring it from metadata. Document the adapter correction without closing upstream compatibility bugs. Add a relocatable Bash fixture and staging/commit regression across SHA-1 and SHA-256 with files and reftable references. All four regressions failed on each backend before the change. Also execute linked-worktree regression fixtures at their final location because their absolute registrations cannot be safely copied between tests. The repository and downstream regression suites passed 145 tests, two skips and 14 subtests on CLI. Adding native/canonical gitfile tests passed 247 tests, two skips and 14 subtests with Gix. Both original Mistral Vibe regressions passed on each backend. Ruff lint/format, ShellCheck and mypy passed with Python 3.12.15, Git 2.53.0 and GixPython 0.2.0. The remaining downstream bare cleanup failure is a GC-timing assumption: both released and local GitPython call `close()` from finalization, but at different times.
1 parent 30013e9 commit 46c4bed

5 files changed

Lines changed: 51 additions & 6 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ The compatibility `entries` property projects those queries into a read-only
5050
snapshot keyed by path and stage, preserving readers in dlt and Renku. It uses
5151
the same Gix-backed default-index query or documented Git fallback as
5252
`iter_entries()`; snapshots do not become editable index state.
53+
54+
Discovery distinguishes a checkout's `.git` file from an arbitrary gitfile
55+
operand. Gix reopens the canonical checkout gitfile through `gix.open_opts()`
56+
and returns `Repository.workdir()` for separate Git directories. Reopening only
57+
the administrative directory loses that checkout context. Standalone gitfile
58+
helpers still reopen the canonical native Git directory to avoid the known
59+
arbitrary-gitfile mismatch (GIX-14). The CLI path uses `rev-parse --show-toplevel`
60+
from the supplied checkout. Mistral Vibe 3.0.1 and scripted SHA-1/SHA-256,
61+
files/reftable regressions verify this adapter correction with Git 2.53.0 and
62+
GixPython 0.2.0; it does not close GIX-14's upstream compatibility bugs.
5363
Deferred/virtual state consists of opaque file bytes, materialized before Git
5464
operates and read back only after success. Private edits use Git because native
5565
bindings cannot load an arbitrary existing index (GIX-3) or validate a checked

‎git/_backend.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _fallback(method: str, reason: str) -> Any:
5757
return NotImplemented
5858

5959

60-
def discover_repository(path: str, environment: Dict[str, Any]) -> Any:
60+
def discover_repository(path: str, environment: Dict[str, Any], *, from_worktree: bool = False) -> Any:
6161
"""Open one discovery candidate; the caller controls parent traversal."""
6262
if gix is None:
6363
return NotImplemented
@@ -73,7 +73,11 @@ def discover_repository(path: str, environment: Dict[str, Any]) -> Any:
7373
)
7474
try:
7575
repo = gix.open_opts(path, options)
76-
if os.path.isfile(path):
76+
if os.path.isfile(path) and from_worktree:
77+
# Canonicalize the supplied checkout path and let Gix resolve both
78+
# its Git directory and worktree from that .git file.
79+
repo = gix.open_opts(os.path.realpath(path), options)
80+
elif os.path.isfile(path):
7781
# Reopening also makes Gix derive the worktree from its Git directory,
7882
# rather than retaining an arbitrary gitfile's location as a worktree.
7983
repo = gix.open_opts(os.path.realpath(repo.git_dir()), options)

‎git/repo/base.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def __init__(
327327
try:
328328
# Git resolves relative gitfile targets using the last forward
329329
# slash in this operand, including on Windows.
330-
native = _backend.discover_repository(candidate, environment)
330+
native = _backend.discover_repository(candidate, environment, from_worktree=candidate == dotgit)
331331
git_dir = (
332332
probe._call_process_safe("rev_parse", "--resolve-git-dir", to_native_path_linux(candidate))
333333
if native is NotImplemented
@@ -358,6 +358,14 @@ def __init__(
358358
if git_dir is None:
359359
raise InvalidGitRepositoryError(epath)
360360

361+
checkout = None
362+
if native is NotImplemented and candidate == dotgit and osp.isfile(candidate):
363+
# Let Git select the checkout through the supplied .git file. Its
364+
# administrative directory alone need not identify the worktree.
365+
checkout_probe = self.GitCommandWrapperType(curpath)
366+
checkout_probe._environment.update(GIT_DIR=None, **environment)
367+
checkout = checkout_probe._call_process_safe("rev_parse", "--show-toplevel")
368+
361369
self.git_dir = git_dir
362370
probe.update_environment(GIT_DIR=git_dir)
363371
try:
@@ -388,7 +396,7 @@ def __init__(
388396
# Gix's configured bare flag also applies to linked worktrees.
389397
self._bare = native.is_bare() and self._working_tree_dir is None
390398
else:
391-
self._working_tree_dir = environment.get("GIT_WORK_TREE")
399+
self._working_tree_dir = environment.get("GIT_WORK_TREE") or checkout
392400
if self._working_tree_dir is None and not self._bare and environment.get("GIT_COMMON_DIR") is None:
393401
try:
394402
probe._call_process_safe("config", "--get", "core.worktree")

‎test/test_downstream_compatibility.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ def test_index_entries_readers_retain_pending_and_published_snapshots(object_for
3636
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
3737
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
3838
def test_git_directory_option_selects_the_requested_worktree(object_format, ref_format):
39-
with fixture_repo("downstream-worktree.sh", object_format, ref_format, write=True) as repo:
39+
# Linked-worktree registration contains absolute paths that cannot be copied.
40+
with fixture_repo("downstream-worktree.sh", object_format, ref_format, write=True, execute=True) as repo:
4041
root = Path(repo.working_tree_dir).parent
4142
linked = root / "linked"
4243
original_index = Path(repo.index.path).read_bytes()
@@ -55,11 +56,24 @@ def test_git_directory_option_selects_the_requested_worktree(object_format, ref_
5556

5657

5758
def test_git_directory_option_retains_explicit_environment():
58-
with fixture_repo("downstream-worktree.sh", "sha1", "files", write=True) as repo:
59+
# Linked-worktree registration contains absolute paths that cannot be copied.
60+
with fixture_repo("downstream-worktree.sh", "sha1", "files", write=True, execute=True) as repo:
5961
root = Path(repo.working_tree_dir).parent
6062
other = root / "other"
6163
with repo.git.custom_environment(GIT_DIR=repo.git_dir, GIT_WORK_TREE=repo.working_tree_dir):
6264
assert repo.git(C=str(other)).show("HEAD:tracked") == "original"
6365
assert repo.git(C=str(other)).show("HEAD:tracked") == "other"
6466
assert repo.git(C=str(other)).show("HEAD:tracked", env={"GIT_DIR": repo.git_dir}) == "original"
6567

68+
69+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
70+
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
71+
def test_separate_git_directory_retains_the_supplied_checkout(object_format, ref_format):
72+
with fixture_repo("downstream-separate-git-dir.sh", object_format, ref_format, write=True) as repo:
73+
assert Path(repo.working_tree_dir).name == "repo"
74+
assert Path(repo.git_dir).name == "metadata"
75+
assert repo.git.rev_parse("--show-toplevel") == repo.working_tree_dir
76+
assert repo.index.entry("tracked").to_blob(repo).data_stream.read() == b"tracked"
77+
Path(repo.working_tree_dir, "tracked").write_text("updated")
78+
repo.index.add(["tracked"])
79+
assert repo.index.commit("updated").tree["tracked"].data_stream.read() == b"updated"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init -q --object-format="$1" --ref-format="$2" --separate-git-dir=metadata repo
4+
# Keep the fixture relocatable when copied into a writable test directory.
5+
printf 'gitdir: ../metadata\n' > repo/.git
6+
cd repo
7+
printf tracked > tracked
8+
git add tracked
9+
git commit -qm initial

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL