Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
.pytest_cache/
29 changes: 29 additions & 0 deletions post-checkout
Original file line number Diff line number Diff line change
Expand Up @@ -620,12 +620,41 @@ def main() -> int:
if prev_ref != "0" * 40:
return 0

# Skip on git clone — only run for worktree creation.
# In a linked worktree, GIT_DIR differs from GIT_COMMON_DIR.
# In a clone (or main worktree), they are the same.
try:
git_dir = subprocess.run(
["git", "rev-parse", "--path-format=absolute", "--git-dir"],
capture_output=True, text=True, check=True,
).stdout.strip()
git_common = subprocess.run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
capture_output=True, text=True, check=True,
).stdout.strip()
if Path(git_dir).resolve() == Path(git_common).resolve():
return 0
except (subprocess.CalledProcessError, FileNotFoundError):
return 0

# Find source worktree
source_worktree = find_source_worktree()
if not source_worktree:
print("Warning: Could not detect source worktree", file=sys.stderr)
return 0

# Hard gate: verify source is in the same repository.
# Defense in depth — individual detection methods already check this,
# but we enforce it here as a final safety net to prevent cross-repo copying.
source_common = _get_git_common_dir(source_worktree)
current_common = _get_git_common_dir()
if not source_common or not current_common or source_common != current_common:
print(
f"Warning: Source worktree {source_worktree} is in a different repository. Skipping.",
file=sys.stderr,
)
return 0

# Get repository root for pattern file lookup
try:
result = subprocess.run(
Expand Down
140 changes: 140 additions & 0 deletions tests/test_clone_and_cross_repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Tests for clone detection and cross-repo safety gate."""

import os
import subprocess
from pathlib import Path

import pytest


def _get_hooks_path() -> str:
return str(Path(__file__).resolve().parents[1])


def _init_repo(path: Path, env: dict) -> None:
"""Initialize a git repo with a commit, .worktreeinclude, and .env."""
path.mkdir(parents=True, exist_ok=True)
subprocess.run(
["git", "init", "-b", "main"],
cwd=path, env=env, check=True, capture_output=True,
)
for key, val in [
("user.email", "test@test.com"),
("user.name", "Test"),
("core.hooksPath", _get_hooks_path()),
]:
subprocess.run(
["git", "config", key, val],
cwd=path, env=env, check=True, capture_output=True,
)
(path / "file.txt").write_text("hello")
(path / ".worktreeinclude").write_text(".env\n")
subprocess.run(
["git", "add", "file.txt", ".worktreeinclude"],
cwd=path, env=env, check=True, capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "init"],
cwd=path, env=env, check=True, capture_output=True,
)
(path / ".env").write_text("SECRET=from_source")


def test_clone_does_not_trigger_file_copy(
tmp_path: Path, isolated_home: Path
):
"""
git clone triggers post-checkout with all-zeros prev_ref, but the hook
should detect it's a clone (not a worktree) and skip file copying.
"""
env = os.environ.copy()
env["HOME"] = str(isolated_home)
(isolated_home / ".worktreeinclude").write_text(".env\n")

source = tmp_path / "source-repo"
_init_repo(source, env)

clone_path = tmp_path / "cloned-repo"
subprocess.run(
["git", "clone", str(source), str(clone_path)],
env=env, check=True, capture_output=True,
)

assert not (clone_path / ".env").exists(), (
".env should NOT be copied during git clone"
)


def test_cross_repo_worktree_rejects_source_and_logs_warning(
tmp_path: Path, isolated_home: Path
):
"""
When GIT_WORKTREE_SOURCE points to a different repository, the hook
should reject it (log a warning) and fall back to same-repo detection.
Files from the cross-repo source must never be copied.
"""
env = os.environ.copy()
env["HOME"] = str(isolated_home)
(isolated_home / ".worktreeinclude").write_text(".env\n")

repo_a = tmp_path / "repo-a"
_init_repo(repo_a, env)
(repo_a / ".env").write_text("SECRET=from_repo_a")

repo_b = tmp_path / "repo-b"
_init_repo(repo_b, env)
# repo-b has no .env — so if anything is copied, it came from repo-a
(repo_b / ".env").unlink()

worktree_path = tmp_path / "repo-b-worktree"
env_with_source = env.copy()
env_with_source["GIT_WORKTREE_SOURCE"] = str(repo_a)

result = subprocess.run(
["git", "worktree", "add", str(worktree_path), "-b", "cross-test"],
cwd=repo_b, env=env_with_source,
check=True, capture_output=True, text=True,
)

assert not (worktree_path / ".env").exists(), (
".env from repo-a should NOT be copied into repo-b worktree"
)
assert "Ignoring invalid GIT_WORKTREE_SOURCE" in result.stderr


def test_worktree_in_path_named_worktrees_still_works(
tmp_path: Path, isolated_home: Path
):
"""
A repo whose filesystem path contains 'worktrees' should still work
correctly for both worktree creation (copy) and clone (skip).
"""
env = os.environ.copy()
env["HOME"] = str(isolated_home)
(isolated_home / ".worktreeinclude").write_text(".env\n")

repo = tmp_path / "worktrees" / "myrepo"
_init_repo(repo, env)

# Worktree creation should copy
wt_path = tmp_path / "worktrees" / "myrepo-wt"
env_with_source = env.copy()
env_with_source["GIT_WORKTREE_SOURCE"] = str(repo)
subprocess.run(
["git", "worktree", "add", str(wt_path), "-b", "test-wt"],
cwd=repo, env=env_with_source,
check=True, capture_output=True,
)
assert (wt_path / ".env").exists(), (
".env should be copied for worktree even in 'worktrees' path"
)

# Clone should NOT copy
clone_path = tmp_path / "worktrees" / "cloned"
subprocess.run(
["git", "clone", str(repo), str(clone_path)],
env=env, check=True, capture_output=True,
)
assert not (clone_path / ".env").exists(), (
".env should NOT be copied during clone even in 'worktrees' path"
)
Loading