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
25 changes: 25 additions & 0 deletions test/lib/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"GIT_REPO",
"GIT_DAEMON_PORT",
"xfail_if_raises",
"symlinks_supported",
"requires_symlinks",
]

import contextlib
Expand All @@ -29,6 +31,7 @@
import logging
import os
import os.path as osp
from stat import S_ISLNK, ST_MODE
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -491,6 +494,28 @@ def _executable(self, basename):
raise RuntimeError(f"no regular file or symlink {path!r}")


def symlinks_supported() -> bool:
"""Check whether this process can actually create a symlink.

On Windows the platform alone doesn't decide it: creating a symlink needs either
Developer Mode or SeCreateSymbolicLinkPrivilege, and an unprivileged process gets
OSError (WinError 1314) instead.
"""
with tempfile.TemporaryDirectory(prefix="gitpython-symlink-check-") as temp_dir:
link_path = osp.join(temp_dir, "link")
try:
os.symlink("missing-target", link_path)
except (NotImplementedError, OSError):
return False
return S_ISLNK(os.lstat(link_path)[ST_MODE])


requires_symlinks = pytest.mark.skipif(
not symlinks_supported(),
reason="symlinks are unavailable, or need privileges this process doesn't have",
)


@contextlib.contextmanager
def xfail_if_raises(
condition: bool,
Expand Down
17 changes: 2 additions & 15 deletions test/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from git.util import Actor, cwd, hex_to_bin, rmtree

from test.lib import TestBase, VirtualEnvironment, fixture, fixture_path, with_rw_directory, with_rw_repo, PathLikeMock
from test.lib.helper import xfail_if_raises
from test.lib.helper import symlinks_supported, xfail_if_raises

HOOKS_SHEBANG = "#!/usr/bin/env sh\n"

Expand Down Expand Up @@ -175,19 +175,6 @@ def _decode(stdout):
_win_bash_status = WinBashStatus.check()


def _windows_supports_symlinks():
if sys.platform != "win32":
return False

with tempfile.TemporaryDirectory(prefix="gitpython-symlink-check-") as temp_dir:
link_path = osp.join(temp_dir, "link")
try:
os.symlink("missing-target", link_path)
except (NotImplementedError, OSError):
return False
return S_ISLNK(os.lstat(link_path)[ST_MODE])


def _make_hook(git_dir, name, content, make_exec=True):
"""A helper to create a hook"""
hp = hook_path(name, git_dir)
Expand Down Expand Up @@ -613,7 +600,7 @@ def _count_existing(self, repo, files):
@with_rw_repo("0.1.6")
def test_index_mutation(self, rw_repo):
with xfail_if_raises(
sys.platform == "win32" and (Git().config("core.symlinks") == "true" or _windows_supports_symlinks()),
sys.platform == "win32" and (Git().config("core.symlinks") == "true" or symlinks_supported()),
raises=(FileNotFoundError, GitCommandError),
reason="Assumes symlinks are not created on Windows and opens a symlink to a nonexistent target.",
):
Expand Down
3 changes: 2 additions & 1 deletion test/test_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import os
import subprocess

from test.lib import TestBase, VirtualEnvironment, with_rw_directory
from test.lib import TestBase, VirtualEnvironment, requires_symlinks, with_rw_directory


class TestInstallation(TestBase):
@requires_symlinks
@with_rw_directory
def test_installation(self, rw_dir):
venv, run = self._set_up_venv(rw_dir)
Expand Down
8 changes: 3 additions & 5 deletions test/test_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import git.refs as refs
from git.util import Actor

from test.lib import TestBase, with_rw_repo, PathLikeMock
from test.lib import TestBase, requires_symlinks, with_rw_repo, PathLikeMock


class TestRefs(TestBase):
Expand Down Expand Up @@ -735,6 +735,7 @@ def test_symbolic_reference_log_append_rejects_path_traversal(self):
)
assert not outside_path.exists()

@requires_symlinks
def test_symbolic_reference_set_reference_rejects_symlink_escape(self):
with tempfile.TemporaryDirectory() as tmp_dir:
base_dir = Path(tmp_dir)
Expand All @@ -746,10 +747,7 @@ def test_symbolic_reference_set_reference_rejects_symlink_escape(self):
refs_heads_dir = Path(repo.common_dir) / "refs" / "heads"
refs_heads_dir.mkdir(parents=True, exist_ok=True)
symlink_path = refs_heads_dir / "link_out"
try:
symlink_path.symlink_to(outside_dir, target_is_directory=True)
except (OSError, NotImplementedError) as ex:
self.skipTest("symlinks unavailable on this platform: %s" % ex)
symlink_path.symlink_to(outside_dir, target_is_directory=True)
if osp.realpath(symlink_path / "escaped") == osp.abspath(symlink_path / "escaped"):
self.skipTest("realpath does not resolve directory symlinks on this platform")

Expand Down
3 changes: 2 additions & 1 deletion test/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from git.repo.fun import touch
from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree

from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo, PathLikeMock
from test.lib import TestBase, fixture, requires_symlinks, with_rw_directory, with_rw_repo, PathLikeMock


def iter_flatten(lol):
Expand Down Expand Up @@ -1351,6 +1351,7 @@ def test_ignored_items_reported(self):
["included_file.txt", "ignored_file.txt", "included_dir/file.txt", "ignored_dir/file.txt"]
) == ["ignored_file.txt", "ignored_dir/file.txt"]

@requires_symlinks
def test_ignored_raises_error_w_symlink(self):
with tempfile.TemporaryDirectory() as tdir:
tmp_dir = pathlib.Path(tdir)
Expand Down
3 changes: 2 additions & 1 deletion test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
rmtree,
)

from test.lib import TestBase, with_rw_repo
from test.lib import TestBase, requires_symlinks, with_rw_repo


@pytest.fixture
Expand Down Expand Up @@ -113,6 +113,7 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
sys.platform == "cygwin",
reason="Cygwin can't set the permissions that make the test meaningful.",
)
@requires_symlinks
def test_avoids_changing_permissions_outside_tree(self, tmp_path, request):
# Automatically works on Windows, but on Unix requires either special handling
# or refraining from attempting to fix PermissionError by making chmod calls.
Expand Down
Loading