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

fix(cmd): honor global `-C` checkout selection · gitpython-developers/GitPython@30013e9 · GitHub

Repository navigation

Commit 30013e9

Browse files
authored andcommitted
fix(cmd): honor global -C checkout selection
Mistral Vibe 3.0.1 creates linked worktrees with `--no-checkout` and then uses `repo.git(C=target).checkout(..., as_process=True)` to populate them. This branch automatically exports the original `GIT_DIR` and `GIT_WORK_TREE` on `repo.git`, so Git ignores the requested checkout for repository discovery. The result leaves the new worktree empty and can overwrite uncommitted files in the original checkout. Released GitPython 3.2.1 populates the linked worktree and preserves those original files. Track automatically supplied repository environment values separately from caller overrides. Drop only those values when `_call_process` supplies a global `-C` option; ordinary calls retain their bound repository. Explicit `update_environment`, temporary `custom_environment` and per-call overrides continue to take precedence. Keep the override bookkeeping after temporary contexts so later calls select their checkout correctly. Repository selection remains Git CLI behavior, including asynchronous and persistent-option calls. Add scripted regressions for linked checkout population, preservation of dirty original files and the published index, different-repository selection, persistent global options and explicit environment precedence. All five new regressions failed before the fix. The adjacent command, repository and guard suites passed 383 tests, three skips and 14 subtests on each backend, CLI first, with Python 3.12.15, Git 2.53.0 and GixPython 0.2.0. Ruff, ShellCheck and mypy passed; basedpyright reported no errors and one existing unresolved `typing_extensions` source warning. The downstream rerun confirms 22 related Mistral Vibe failures are fixed; separate-directory metadata and its GC-dependent cleanup assertion are being investigated separately.
1 parent b3a4ba8 commit 30013e9

5 files changed

Lines changed: 68 additions & 1 deletion

File tree

‎doc/source/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ Git executable and safety
3838
suppresses configured signing by default; signing, verification, and editor
3939
options require the same opt-in.
4040
* The raw ``repo.git`` interface remains available for direct Git commands.
41+
Global ``-C`` options select the requested checkout without the repository
42+
locations automatically supplied by ``Repo`` overriding it. Explicit
43+
environment overrides retain Git's precedence over ``-C``.
4144
Existing explicit unsafe-option and unsafe-protocol opt-ins remain independent.
4245
They do not permit argument or stdin-record injection, or reordering command
4346
options past the library's safety flags.

‎git/cmd.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ class Git:
616616
"_git_options",
617617
"_persistent_git_options",
618618
"_environment",
619+
"_repository_environment",
619620
"_repo",
620621
)
621622

@@ -1164,6 +1165,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
11641165

11651166
# Extra environment variables to pass to git commands
11661167
self._environment: Dict[str, Optional[str]] = {}
1168+
self._repository_environment: Dict[str, Optional[str]] = {}
11671169
self._repo: Any = None # Weak reference; the Repo owns native resources.
11681170

11691171
# Cached version slots
@@ -1535,7 +1537,11 @@ def execute(
15351537
# just to be sure.
15361538
environment["LANGUAGE"] = "C"
15371539
environment["LC_ALL"] = "C"
1538-
environment.update(self._environment)
1540+
redirected = subprocess_kwargs.pop("_repository_redirected", False)
1541+
automatic = self._repository_environment if redirected else {}
1542+
environment.update(
1543+
{key: value for key, value in self._environment.items() if key not in automatic or value != automatic[key]}
1544+
)
15391545
if inline_env is not None:
15401546
environment.update(inline_env)
15411547
# Internal or per-call None overrides remove inherited variables.
@@ -1725,6 +1731,7 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
17251731
"""
17261732
old_env = {}
17271733
for key, value in kwargs.items():
1734+
self._repository_environment.pop(key, None)
17281735
# Set value if it is None.
17291736
if value is not None:
17301737
old_env[key] = self._environment.get(key)
@@ -1748,11 +1755,13 @@ def custom_environment(self, **kwargs: Any) -> Iterator[None]:
17481755
:param kwargs:
17491756
See :meth:`update_environment`.
17501757
"""
1758+
automatic = {key: self._repository_environment[key] for key in kwargs if key in self._repository_environment}
17511759
old_env = self.update_environment(**kwargs)
17521760
try:
17531761
yield
17541762
finally:
17551763
self.update_environment(**old_env)
1764+
self._repository_environment.update(automatic)
17561765

17571766
def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]:
17581767
if len(name) == 1:
@@ -1907,6 +1916,11 @@ def _call_process(
19071916
call.extend(self._git_options)
19081917
self._git_options = ()
19091918

1919+
# Only global options redirect discovery. A subcommand operand named
1920+
# -C must not drop the repository locations supplied by Repo.
1921+
if any(option == "-C" for option in call[1:]):
1922+
exec_kwargs["_repository_redirected"] = True
1923+
19101924
call.extend(safe_git_options)
19111925

19121926
call.append(dashify(method))

‎git/repo/base.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,12 @@ def __init__(
447447
self.git = self.GitCommandWrapperType(self.working_dir)
448448
self.git._repo = weakref.ref(self)
449449
self.git._environment.update(GIT_DIR=git_dir, **environment)
450+
if git_dir_env is None:
451+
self.git._repository_environment["GIT_DIR"] = git_dir
450452
if self._working_tree_dir is not None:
451453
self.git.update_environment(GIT_WORK_TREE=os.fspath(self._working_tree_dir))
454+
if "GIT_WORK_TREE" not in environment:
455+
self.git._repository_environment["GIT_WORK_TREE"] = os.fspath(self._working_tree_dir)
452456
if native is not NotImplemented:
453457
self._gix_repository = native
454458
self._empty_tree_hexsha = str(native.empty_tree().id)

‎test/test_downstream_compatibility.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,35 @@ def test_index_entries_readers_retain_pending_and_published_snapshots(object_for
3131
index.write()
3232
assert repo.index.entries == pending
3333
assert entries[("tracked", 0)].binsha == repo.head.commit.tree["tracked"].binsha
34+
35+
36+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
37+
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
38+
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:
40+
root = Path(repo.working_tree_dir).parent
41+
linked = root / "linked"
42+
original_index = Path(repo.index.path).read_bytes()
43+
assert not (linked / "tracked").exists()
44+
process = repo.git(C=str(linked)).checkout("--force", as_process=True)
45+
process.wait()
46+
assert (linked / "tracked").read_text() == "original"
47+
assert Path(repo.working_tree_dir, "tracked").read_text() == "unchanged"
48+
assert Path(repo.index.path).read_bytes() == original_index
49+
assert repo.git(C=str(linked)).rev_parse("--show-toplevel") == str(linked)
50+
assert repo.git(C=str(root / "other")).show("HEAD:tracked") == "other"
51+
repo.git.set_persistent_git_options(C=str(linked))
52+
assert repo.git.rev_parse("--show-toplevel") == str(linked)
53+
repo.git.set_persistent_git_options()
54+
assert repo.git.rev_parse("--show-toplevel") == repo.working_tree_dir
55+
56+
57+
def test_git_directory_option_retains_explicit_environment():
58+
with fixture_repo("downstream-worktree.sh", "sha1", "files", write=True) as repo:
59+
root = Path(repo.working_tree_dir).parent
60+
other = root / "other"
61+
with repo.git.custom_environment(GIT_DIR=repo.git_dir, GIT_WORK_TREE=repo.working_tree_dir):
62+
assert repo.git(C=str(other)).show("HEAD:tracked") == "original"
63+
assert repo.git(C=str(other)).show("HEAD:tracked") == "other"
64+
assert repo.git(C=str(other)).show("HEAD:tracked", env={"GIT_DIR": repo.git_dir}) == "original"
65+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init -q --object-format="$1" --ref-format="$2" repo
4+
cd repo
5+
printf original > tracked
6+
git add tracked
7+
git commit -qm initial
8+
git worktree add -q --no-checkout -b linked ../linked HEAD
9+
printf unchanged > tracked
10+
git init -q ../other
11+
cd ../other
12+
printf other > tracked
13+
git add tracked
14+
git commit -qm other

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL