"""High-level operands must not become Git options."""
from unittest import mock
import pytest
from git import Actor, Git, GitCommandError, Head, Remote, RemoteReference, Repo, TagReference
from git.exc import UnsafeOptionError
@pytest.mark.parametrize("allow_unsafe_options", [False, True])
@pytest.mark.parametrize(
"refspec",
["--upload-pack=helper", ["--upl=helper"], ["main", "--dry-run"], "-uhelper", "--arg value", "--"],
)
def test_pull_rejects_option_shaped_refspec(tmp_path, refspec, allow_unsafe_options):
repo = Repo.init(tmp_path)
remote = Remote(repo, "origin")
with mock.patch.object(Git, "_call_process", side_effect=AssertionError("Git must not run")) as run:
with pytest.raises(UnsafeOptionError):
remote.pull(refspec, allow_unsafe_options=allow_unsafe_options)
run.assert_not_called()
def test_pull_rejects_option_shaped_remote(tmp_path):
repo = Repo.init(tmp_path)
remote = Remote(repo, "--upload-pack=helper")
with mock.patch.object(Git, "_call_process", side_effect=AssertionError("Git must not run")) as run:
with pytest.raises(UnsafeOptionError):
remote.pull("main")
run.assert_not_called()
def test_pull_preserves_operand_and_explicit_option_values(tmp_path):
repo = Repo.init(tmp_path)
remote = Remote(repo, "origin")
with mock.patch.object(Git, "_call_process") as run, mock.patch.object(
Remote, "_get_fetch_info_from_stderr", return_value=[]
):
remote.pull("refs/heads/topic", upload_pack="helper with spaces", allow_unsafe_options=True)
assert run.call_args[0] == ("pull", "--", remote, ["refs/heads/topic"])
assert run.call_args[1]["upload_pack"] == "helper with spaces"
def test_delete_head_cannot_override_force(tmp_path):
repo = Repo.init(tmp_path)
actor = Actor("Test", "test@example.com")
initial = repo.index.commit("initial", author=actor, committer=actor)
branch = repo.create_head("unmerged", initial)
branch.commit = repo.index.commit("unmerged", head=False, author=actor, committer=actor)
with pytest.raises(GitCommandError):
repo.delete_head("--force", branch, force=False)
assert branch.is_valid()
repo.delete_head(branch, force=True)
assert not branch.is_valid()
def test_rename_head_cannot_select_current_branch(tmp_path):
repo = Repo.init(tmp_path)
actor = Actor("Test", "test@example.com")
repo.index.commit("initial", author=actor, committer=actor)
original = repo.active_branch.name
with pytest.raises(GitCommandError):
Head(repo, "refs/heads/--force").rename("renamed")
assert repo.active_branch.name == original
def test_tag_operands_follow_option_terminator(tmp_path):
repo = Repo.init(tmp_path)
with mock.patch.object(Git, "_call_process") as run:
TagReference.create(repo, "topic", "HEAD")
assert run.call_args[0] == ("tag", "--", "topic", "HEAD")
TagReference.delete(repo, "--list")
assert run.call_args[0] == ("tag", "-d", "--", "--list")
def test_remote_ref_delete_preserves_operand(tmp_path):
repo = Repo.init(tmp_path)
ref = RemoteReference(repo, "refs/remotes/--force")
with mock.patch.object(Git, "_call_process") as run:
RemoteReference.delete(repo, ref)
assert run.call_args[0] == ("branch", "-d", "-r", "--", ref)
def test_move_treats_option_shaped_source_as_filename(tmp_path):
repo = Repo.init(tmp_path)
(tmp_path / "--force").write_text("literal source")
repo.index.add(["--force"])
assert repo.index.move(["--force", "destination"]) == [("--force", "destination")]
assert (tmp_path / "destination").read_text() == "literal source"
assert not (tmp_path / "--force").exists()
def test_move_cannot_override_overwrite_protection(tmp_path):
repo = Repo.init(tmp_path)
for name in ("--force", "source", "destination"):
(tmp_path / name).write_text(name)
repo.index.add(["--force", "source", "destination"])
with pytest.raises(GitCommandError):
repo.index.move(["--force", "source", "destination"])
assert (tmp_path / "source").read_text() == "source"
assert (tmp_path / "destination").read_text() == "destination"
repo.index.move(["source", "destination"], force=True)
assert (tmp_path / "destination").read_text() == "source"
def test_ignored_treats_option_shaped_path_as_filename(tmp_path):
repo = Repo.init(tmp_path)
(tmp_path / ".gitignore").write_text("--verbose\n--arg value\n")
assert repo.ignored("--verbose", "--arg value") == ["--verbose", "--arg value"]
def test_move_cannot_override_dry_run(tmp_path):
repo = Repo.init(tmp_path)
(tmp_path / "source").write_text("source")
repo.index.add(["source"])
with pytest.raises(GitCommandError):
repo.index.move(["--no-dry-run", "source", "destination"], dry_run=True)
assert (tmp_path / "source").read_text() == "source"
assert not (tmp_path / "destination").exists()
@pytest.mark.parametrize("name", ["--prune", "--all", "--upload-pack=helper"])
def test_remote_update_rejects_option_shaped_name(tmp_path, name):
repo = Repo.init(tmp_path)
with mock.patch.object(Git, "_call_process", side_effect=AssertionError("Git must not run")) as run:
with pytest.raises(UnsafeOptionError):
Remote(repo, name).update()
run.assert_not_called()