GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

No menu could be recovered. Follow the blue links in the page.

This view

  • Original page
  • New address

GeezerVison is showing raw.githubusercontent.com — raw.githubusercontent.com

# Copyright (C) 2008, 2009 Michael Trier ([email protected]) and contributors # # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ import gc import os import os.path as osp import random import socket import sys import tempfile import threading import time from pathlib import Path from unittest import mock, skipIf import pytest from git import ( Commit, FetchInfo, GitCommandError, Head, PushInfo, Reference, Remote, RemoteProgress, RemoteReference, SymbolicReference, TagReference, ) from git.cmd import Git from git.exc import UnsafeOptionError, UnsafeProtocolError from git.util import IterableList from test.cleanup import cleanup_directory from test.lib import ( GIT_DAEMON_PORT, TestBase, fixture, with_rw_and_rw_remote_repo, with_rw_repo, ) from test.lib.fixtures import fixture_repo, writable_fixture # Make sure we have repeatable results. random.seed(0) class TestRemoteProgress(RemoteProgress): __slots__ = ("_seen_lines", "_stages_per_op", "_num_progress_messages") def __init__(self) -> None: super().__init__() self._seen_lines = [] self._stages_per_op = {} self._num_progress_messages = 0 def _parse_progress_line(self, line): # We may remove the line later if it is dropped. # Keep it for debugging. self._seen_lines.append(line) rval = super()._parse_progress_line(line) return rval def line_dropped(self, line): try: self._seen_lines.remove(line) except ValueError: pass def update(self, op_code, cur_count, max_count=None, message=""): # Check each stage only comes once. op_id = op_code & self.OP_MASK assert op_id in (self.COUNTING, self.COMPRESSING, self.WRITING) if op_code & self.WRITING > 0: if op_code & self.BEGIN > 0: assert not message, "should not have message when remote begins writing" elif op_code & self.END > 0: assert message assert not message.startswith(", "), "Sanitize progress messages: '%s'" % message assert not message.endswith(", "), "Sanitize progress messages: '%s'" % message self._stages_per_op.setdefault(op_id, 0) self._stages_per_op[op_id] = self._stages_per_op[op_id] | (op_code & self.STAGE_MASK) if op_code & (self.WRITING | self.END) == (self.WRITING | self.END): assert message # END check we get message self._num_progress_messages += 1 def make_assertion(self): # We don't always receive messages. if not self._seen_lines: return # Sometimes objects are not compressed which is okay. assert len(self._seen_ops) in (2, 3), len(self._seen_ops) assert self._stages_per_op # Must have seen all stages. for _op, stages in self._stages_per_op.items(): assert stages & self.STAGE_MASK == self.STAGE_MASK # END for each op/stage def assert_received_message(self): assert self._num_progress_messages TestRemoteProgress.__test__ = False # type: ignore class TestRemote(TestBase): def tearDown(self): gc.collect() def _print_fetchhead(self, repo): with open(osp.join(repo.git_dir, "FETCH_HEAD")): pass def _do_test_fetch_result(self, results, remote): # self._print_fetchhead(remote.repo) self.assertGreater(len(results), 0) self.assertIsInstance(results[0], FetchInfo) for info in results: self.assertIsInstance(info.note, str) if isinstance(info.ref, Reference): self.assertTrue(info.flags) # END reference type flags handling self.assertIsInstance(info.ref, (SymbolicReference, Reference)) if info.flags & (info.FORCED_UPDATE | info.FAST_FORWARD): self.assertIsInstance(info.old_commit, Commit) else: self.assertIsNone(info.old_commit) # END forced update checking # END for each info def _do_test_push_result(self, results, remote): self.assertIsInstance(results, list) self.assertIsInstance(results, IterableList) self.assertGreater(len(results), 0) self.assertIsInstance(results[0], PushInfo) for info in results: self.assertTrue(info.flags) self.assertIsInstance(info.summary, str) if info.old_commit is not None: self.assertIsInstance(info.old_commit, Commit) if info.flags & info.ERROR: has_one = False for bitflag in ( info.REJECTED, info.REMOTE_REJECTED, info.REMOTE_FAILURE, ): has_one |= bool(info.flags & bitflag) # END for each bitflag self.assertTrue(has_one) else: # There must be a remote commit. if info.flags & info.DELETED == 0: self.assertIsInstance(info.local_ref, Reference) else: self.assertIsNone(info.local_ref) self.assertIn(type(info.remote_ref), (TagReference, RemoteReference)) # END error checking # END for each info if any(info.flags & info.ERROR for info in results): self.assertRaises(GitCommandError, results.raise_if_error) else: # No errors, so this should do nothing. results.raise_if_error() def _do_test_fetch_info(self, repo): self.assertRaises(ValueError, FetchInfo._from_line, repo, "nonsense", "") self.assertRaises( ValueError, FetchInfo._from_line, repo, "? [up to date] 0.1.7RC -> origin/0.1.7RC", "", ) def _commit_random_file(self, repo): """Create a file with a random name and random data and commit it to a repo. :return: The committed absolute file path. """ index = repo.index new_file = self._make_file(osp.basename(tempfile.mktemp()), str(random.random()), repo) index.add([new_file]) index.commit("Committing %s" % new_file) return new_file def _do_test_fetch(self, remote, rw_repo, remote_repo, **kwargs): """Specialized fetch testing to de-clutter the main test.""" self._do_test_fetch_info(rw_repo) def fetch_and_test(remote, **kwargs): progress = TestRemoteProgress() kwargs["progress"] = progress res = remote.fetch(**kwargs) progress.make_assertion() self._do_test_fetch_result(res, remote) return res # END fetch and check def get_info(res, remote, name): return res["%s/%s" % (remote, name)] # Put remote head to master as it is guaranteed to exist. remote_repo.head.reference = remote_repo.heads.master res = fetch_and_test(remote, **kwargs) # All up to date. for info in res: self.assertTrue(info.flags & info.HEAD_UPTODATE) # Rewind remote head to trigger rejection. # index must be false as remote is a bare repo. rhead = remote_repo.head remote_commit = rhead.commit rhead.reset("HEAD~2", index=False) res = fetch_and_test(remote) mkey = "%s/%s" % (remote, "master") master_info = res[mkey] self.assertTrue(master_info.flags & FetchInfo.FORCED_UPDATE) self.assertIsNotNone(master_info.note) # Normal fast forward - set head back to previous one. rhead.commit = remote_commit res = fetch_and_test(remote) self.assertTrue(res[mkey].flags & FetchInfo.FAST_FORWARD) # New remote branch. new_remote_branch = Head.create(remote_repo, "new_branch") res = fetch_and_test(remote) new_branch_info = get_info(res, remote, new_remote_branch) self.assertTrue(new_branch_info.flags & FetchInfo.NEW_HEAD) # Remote branch rename (causes creation of a new one locally). new_remote_branch.rename("other_branch_name") res = fetch_and_test(remote) other_branch_info = get_info(res, remote, new_remote_branch) self.assertEqual(other_branch_info.ref.commit, new_branch_info.ref.commit) # Remove new branch. Head.delete(new_remote_branch.repo, new_remote_branch) res = fetch_and_test(remote) # Deleted remote will not be fetched. self.assertRaises(IndexError, get_info, res, remote, new_remote_branch) # Prune stale tracking branches. stale_refs = remote.stale_refs self.assertEqual(len(stale_refs), 2) self.assertIsInstance(stale_refs[0], RemoteReference) RemoteReference.delete(rw_repo, *stale_refs) # Test single branch fetch with refspec including target remote. res = fetch_and_test(remote, refspec="master:refs/remotes/%s/master" % remote) self.assertEqual(len(res), 1) self.assertTrue(get_info(res, remote, "master")) # ...with respec and no target. res = fetch_and_test(remote, refspec="master") self.assertEqual(len(res), 1) # ...multiple refspecs...works, but git command returns with error if one ref is # wrong without doing anything. This is new in later binaries. # res = fetch_and_test(remote, refspec=['master', 'fred']) # self.assertEqual(len(res), 1) # Add new tag reference. rtag = TagReference.create(remote_repo, "1.0-RV_hello.there") res = fetch_and_test(remote, tags=True) tinfo = res[str(rtag)] self.assertIsInstance(tinfo.ref, TagReference) self.assertEqual(tinfo.ref.commit, rtag.commit) self.assertTrue(tinfo.flags & tinfo.NEW_TAG) # Adjust the local tag commit. Reference.set_object(rtag, rhead.commit.parents[0].parents[0]) # As of git 2.20 one cannot clobber local tags that have changed without # specifying --force, and the test assumes you can clobber, so... force = None if rw_repo.git.version_info[:2] >= (2, 20): force = True res = fetch_and_test(remote, tags=True, force=force) tinfo = res[str(rtag)] self.assertEqual(tinfo.commit, rtag.commit) self.assertTrue(tinfo.flags & tinfo.TAG_UPDATE) # Delete remote tag - local one will stay. TagReference.delete(remote_repo, rtag) res = fetch_and_test(remote, tags=True) self.assertRaises(IndexError, get_info, res, remote, str(rtag)) # Provoke to receive actual objects to see what kind of output we have to # expect. For that we need a remote transport protocol. # Create a new UN-shared repo and fetch into it after we pushed a change to the # shared repo. other_repo_dir = tempfile.mktemp("other_repo") # Must clone with a local path for the repo implementation not to freak out as # it wants local paths only (which I can understand). other_repo = remote_repo.clone(other_repo_dir, shared=False) remote_repo_url = osp.basename(remote_repo.git_dir) # git-daemon runs with appropriate `--base-path`. remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, remote_repo_url)) # Put origin to git-url. other_origin = other_repo.remotes.origin with other_origin.config_writer as cw: cw.set("url", remote_repo_url) # It automatically creates alternates as remote_repo is shared as well. # It will use the transport though and ignore alternates when fetching. # assert not other_repo.alternates # this would fail # Ensure we are in the right state. rw_repo.head.reset(remote.refs.master, working_tree=True) try: self._commit_random_file(rw_repo) remote.push(rw_repo.head.reference) # Here I would expect to see remote-information about packing objects and so # on. Unfortunately, this does not happen if we are redirecting the output - # git explicitly checks for this and only provides progress information to # ttys. res = fetch_and_test(other_origin) finally: other_repo.close() cleanup_directory(other_repo_dir) # END test and cleanup def _assert_push_and_pull(self, remote, rw_repo, remote_repo): # Push our changes. lhead = rw_repo.head # Ensure we are on master and it is checked out where the remote is. try: lhead.reference = rw_repo.heads.master except AttributeError: # If the author is on a non-master branch, the clones might not have a local # master yet. We simply create it. lhead.reference = rw_repo.create_head("master") # END master handling lhead.reset(remote.refs.master, working_tree=True) # Push without spec should fail (without further configuration) # well, works nicely # self.assertRaises(GitCommandError, remote.push) # Simple file push. self._commit_random_file(rw_repo) progress = TestRemoteProgress() res = remote.push(lhead.reference, progress) self.assertIsInstance(res, list) self._do_test_push_result(res, remote) progress.make_assertion() # Rejected - undo last commit. lhead.reset("HEAD~1") res = remote.push(lhead.reference) self.assertTrue(res[0].flags & PushInfo.ERROR) self.assertTrue(res[0].flags & PushInfo.REJECTED) self._do_test_push_result(res, remote) # Force rejected pull. res = remote.push("+%s" % lhead.reference) self.assertEqual(res[0].flags & PushInfo.ERROR, 0) self.assertTrue(res[0].flags & PushInfo.FORCED_UPDATE) self._do_test_push_result(res, remote) # Invalid refspec. self.assertRaises(GitCommandError, remote.push, "hellothere") # Push new tags. progress = TestRemoteProgress() to_be_updated = "my_tag.1.0RV" new_tag = TagReference.create(rw_repo, to_be_updated) # @UnusedVariable other_tag = TagReference.create(rw_repo, "my_obj_tag.2.1aRV", logmsg="my message") res = remote.push(progress=progress, tags=True) self.assertTrue(res[-1].flags & PushInfo.NEW_TAG) progress.make_assertion() self._do_test_push_result(res, remote) # Update push new tags. # Rejection is default. new_tag = TagReference.create(rw_repo, to_be_updated, reference="HEAD~1", force=True) res = remote.push(tags=True) self._do_test_push_result(res, remote) self.assertTrue(res[-1].flags & PushInfo.REJECTED) self.assertTrue(res[-1].flags & PushInfo.ERROR) # Force push this tag. res = remote.push("+%s" % new_tag.path) self.assertEqual(res[-1].flags & PushInfo.ERROR, 0) self.assertTrue(res[-1].flags & PushInfo.FORCED_UPDATE) # Delete tag - have to do it using refspec. res = remote.push(":%s" % new_tag.path) self._do_test_push_result(res, remote) self.assertTrue(res[0].flags & PushInfo.DELETED) # Currently progress is not properly transferred, especially not using # the git daemon. # progress.assert_received_message() # Push new branch. new_head = Head.create(rw_repo, "my_new_branch") progress = TestRemoteProgress() res = remote.push(new_head, progress) self.assertGreater(len(res), 0) self.assertTrue(res[0].flags & PushInfo.NEW_HEAD) progress.make_assertion() self._do_test_push_result(res, remote) # Rejected stale delete. force_with_lease = "%s:0000000000000000000000000000000000000000" % new_head.path res = remote.push(":%s" % new_head.path, force_with_lease=force_with_lease) self.assertTrue(res[0].flags & PushInfo.ERROR) self.assertTrue(res[0].flags & PushInfo.REJECTED) self.assertIsNone(res[0].local_ref) self._do_test_push_result(res, remote) # Delete new branch on the remote end and locally. res = remote.push(":%s" % new_head.path) self._do_test_push_result(res, remote) Head.delete(rw_repo, new_head) self.assertTrue(res[-1].flags & PushInfo.DELETED) # --all res = remote.push(all=True) self._do_test_push_result(res, remote) remote.pull("master", kill_after_timeout=10.0) # Cleanup - delete created tags and branches as we are in an inner loop on # the same repository. TagReference.delete(rw_repo, new_tag, other_tag) remote.push(":%s" % other_tag.path, kill_after_timeout=10.0) @pytest.mark.net @skipIf(sys.platform == "win32", "FIXME: Freezes!") @with_rw_and_rw_remote_repo("release") def test_base(self, rw_repo, remote_repo): num_remotes = 0 remote_set = set() ran_fetch_test = False for remote in rw_repo.remotes: num_remotes += 1 self.assertEqual(remote, remote) self.assertNotEqual(str(remote), repr(remote)) remote_set.add(remote) remote_set.add(remote) # Should already exist. # REFS refs = remote.refs self.assertTrue(refs) for ref in refs: self.assertEqual(ref.remote_name, remote.name) self.assertTrue(ref.remote_head) # END for each ref # OPTIONS # Cannot use 'fetch' key anymore as it is now a method. for opt in ("url",): val = getattr(remote, opt) reader = remote.config_reader assert reader.get(opt) == val assert reader.get_value(opt, None) == val # Unable to write with a reader. self.assertRaises(IOError, reader.set, opt, "test") # Change value. with remote.config_writer as writer: new_val = "myval" writer.set(opt, new_val) assert writer.get(opt) == new_val writer.set(opt, val) assert writer.get(opt) == val assert getattr(remote, opt) == val # END for each default option key # RENAME other_name = "totally_other_name" prev_name = remote.name self.assertEqual(remote.rename(other_name), remote) self.assertNotEqual(prev_name, remote.name) # Multiple times. for _ in range(2): self.assertEqual(remote.rename(prev_name).name, prev_name) # END for each rename ( back to prev_name ) # PUSH/PULL TESTING self._assert_push_and_pull(remote, rw_repo, remote_repo) # FETCH TESTING # Only for remotes - local cases are the same or less complicated as # additional progress information will never be emitted. if remote.name == "daemon_origin": self._do_test_fetch(remote, rw_repo, remote_repo, kill_after_timeout=10.0) ran_fetch_test = True # END fetch test remote.update() # END for each remote self.assertTrue(ran_fetch_test) self.assertTrue(num_remotes) self.assertEqual(num_remotes, len(remote_set)) origin = rw_repo.remote("origin") assert origin == rw_repo.remotes.origin # Verify we can handle prunes when fetching. # stderr lines look like this: x [deleted] (none) -> origin/experiment-2012 # These should just be skipped. # If we don't have a manual checkout, we can't actually assume there are any # non-master branches. remote_repo.create_head("myone_for_deletion") # Get the branch - to be pruned later origin.fetch() num_deleted = False for branch in remote_repo.heads: if branch.name != "master": branch.delete(remote_repo, branch, force=True) num_deleted += 1 # END for each branch self.assertGreater(num_deleted, 0) self.assertEqual( len(rw_repo.remotes.origin.fetch(prune=True)), 1, "deleted everything but master", ) @with_rw_repo("HEAD", bare=True) def test_creation_and_removal(self, bare_rw_repo): new_name = "test_new_one" arg_list = (new_name, "git@server:hello.git") remote = Remote.create(bare_rw_repo, *arg_list) self.assertEqual(remote.name, "test_new_one") self.assertIn(remote, bare_rw_repo.remotes) self.assertTrue(remote.exists()) # Create same one again. self.assertRaises(GitCommandError, Remote.create, bare_rw_repo, *arg_list) Remote.remove(bare_rw_repo, new_name) self.assertTrue(remote.exists()) # We still have a cache that doesn't know we were deleted by name. remote._clear_cache() assert not remote.exists() # Cache should be renewed now. This is an issue... for remote in bare_rw_repo.remotes: if remote.name == new_name: raise AssertionError("Remote removal failed") # END if deleted remote matches existing remote's name # END for each remote # Issue #262 - the next call would fail if bug wasn't fixed. bare_rw_repo.create_remote("bogus", "/bogus/path", mirror="push") def test_fetch_info(self): for source, destination, refspec, ref_type in ( ("master", "origin/master", "+refs/heads/*:refs/remotes/origin/*", RemoteReference), ("tag", "subdir/tag", "refs/tags/tag:refs/tags/subdir/tag", TagReference), ("branch", "refs/something/branch", "refs/heads/branch:refs/something/branch", Reference), ): info = FetchInfo._from_line(self.rorepo, "* [new branch] %s -> %s" % (source, destination), [refspec]) self.assertIsInstance(info.ref, ref_type) self.assertEqual(info.remote_ref_path, source) self.assertTrue(info.flags & FetchInfo.NEW_HEAD) with self.assertRaises(ValueError): FetchInfo._from_line(self.rorepo, "= [up to date] master -> ambiguous") def test_uncommon_branch_names(self): lines = fixture("uncommon_branch_prefix_stderr").decode("ascii").splitlines() results = [FetchInfo._from_line(self.rorepo, line, ["+refs/pull/*:refs/heads/pull/*"]) for line in lines] self.assertEqual(results[0].remote_ref_path, "refs/pull/1/head") self.assertEqual(results[0].ref.path, "refs/heads/pull/1/head") self.assertIsInstance(results[0].ref, Head) @with_rw_repo("HEAD", bare=False) def test_multiple_urls(self, rw_repo): # Test addresses. test1 = "https://github.com/gitpython-developers/GitPython" test2 = "https://github.com/gitpython-developers/gitdb" test3 = "https://github.com/gitpython-developers/smmap" remote = rw_repo.remotes[0] # Test setting a single URL. remote.set_url(test1) self.assertEqual(list(remote.urls), [test1]) # Test replacing that single URL. remote.set_url(test1) self.assertEqual(list(remote.urls), [test1]) # Test adding new URLs. remote.set_url(test2, add=True) self.assertEqual(list(remote.urls), [test1, test2]) remote.set_url(test3, add=True) self.assertEqual(list(remote.urls), [test1, test2, test3]) # Test removing a URL. remote.set_url(test2, delete=True) self.assertEqual(list(remote.urls), [test1, test3]) # Test changing a URL. remote.set_url(test2, test3) self.assertEqual(list(remote.urls), [test1, test2]) # will raise: fatal: --add --delete doesn't make sense self.assertRaises(GitCommandError, remote.set_url, test2, add=True, delete=True) # Test on another remote, with the add/delete URL. remote = rw_repo.create_remote("another", url=test1) remote.add_url(test2) self.assertEqual(list(remote.urls), [test1, test2]) remote.add_url(test3) self.assertEqual(list(remote.urls), [test1, test2, test3]) # Test removing all the URLs. remote.delete_url(test2) self.assertEqual(list(remote.urls), [test1, test3]) remote.delete_url(test1) self.assertEqual(list(remote.urls), [test3]) # Will raise fatal: Will not delete all non-push URLs. self.assertRaises(GitCommandError, remote.delete_url, test3) @with_rw_repo("HEAD", bare=False) def test_create_does_not_expand_environment_variables_in_url(self, rw_repo): url = "https://example.com/${GITPYTHON_TEST_SECRET}/repo.git" with mock.patch.dict(os.environ, {"GITPYTHON_TEST_SECRET": "sensitive-value"}): remote = rw_repo.create_remote("untrusted", url) assert remote.url == url @with_rw_repo("HEAD") def test_fetch_error(self, rw_repo): rem = rw_repo.remote("origin") msg = ( r"[Cc]ouldn't find remote ref __BAD_REF__|" r"could not read Username|" r"expected flush after ref listing" ) with self.assertRaisesRegex(GitCommandError, msg): rem.fetch("__BAD_REF__") @with_rw_repo("release", bare=False) def test_push_error(self, repo): rem = repo.remote("origin") with self.assertRaisesRegex(GitCommandError, "src refspec __BAD_REF__ does not match any"): rem.push("__BAD_REF__") @with_rw_repo("HEAD") def test_set_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): remote.set_url(url) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_set_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: remote.set_url(url, allow_unsafe_protocols=True) assert list(remote.urls)[-1] == url assert not tmp_file.exists() @with_rw_repo("HEAD") def test_add_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): remote.add_url(url) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_add_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: remote.add_url(url, allow_unsafe_protocols=True) assert list(remote.urls)[-1] == url assert not tmp_file.exists() @with_rw_repo("HEAD") def test_create_remote_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): Remote.create(rw_repo, "origin", url) assert not tmp_file.exists() @pytest.mark.xfail( sys.platform == "win32", reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value", raises=AssertionError, ) @with_rw_repo("HEAD") def test_create_remote_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for i, url in enumerate(urls): remote = Remote.create(rw_repo, f"origin{i}", url, allow_unsafe_protocols=True) assert remote.url == url assert not tmp_file.exists() @with_rw_repo("HEAD") def test_fetch_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): remote.fetch(url) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_fetch_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: # The URL will be allowed into the command, but the command will fail # since we don't have that protocol enabled in the Git config file. with self.assertRaises(GitCommandError): remote.fetch(url, allow_unsafe_protocols=True) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_fetch_unsafe_options(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [ {"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}, {"upload_p": f"touch {tmp_file}"}, ] for unsafe_option in unsafe_options: with self.assertRaises(UnsafeOptionError): remote.fetch(**unsafe_option) assert not tmp_file.exists() @pytest.mark.xfail( sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_fetch_unsafe_options must be adjusted in the " "same way. Until then, test_fetch_unsafe_options is unreliable on Windows." ), raises=AssertionError, ) @with_rw_repo("HEAD") def test_fetch_unsafe_options_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [{"upload-pack": f"touch {tmp_file}"}] for unsafe_option in unsafe_options: # The options will be allowed, but the command will fail. assert not tmp_file.exists() with self.assertRaises(GitCommandError): remote.fetch(**unsafe_option, allow_unsafe_options=True) assert tmp_file.exists() tmp_file.unlink() @with_rw_repo("HEAD") def test_pull_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): remote.pull(url) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_pull_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: # The URL will be allowed into the command, but the command will fail # since we don't have that protocol enabled in the Git config file. with self.assertRaises(GitCommandError): remote.pull(url, allow_unsafe_protocols=True) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_pull_unsafe_options(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [ {"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}, {"upload_p": f"touch {tmp_file}"}, ] for unsafe_option in unsafe_options: with self.assertRaises(UnsafeOptionError): remote.pull(**unsafe_option) assert not tmp_file.exists() @pytest.mark.xfail( sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_pull_unsafe_options must be adjusted in the " "same way. Until then, test_pull_unsafe_options is unreliable on Windows." ), raises=AssertionError, ) @with_rw_repo("HEAD") def test_pull_unsafe_options_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [{"upload-pack": f"touch {tmp_file}"}] for unsafe_option in unsafe_options: # The options will be allowed, but the command will fail. assert not tmp_file.exists() with self.assertRaises(GitCommandError): remote.pull(**unsafe_option, allow_unsafe_options=True) assert tmp_file.exists() tmp_file.unlink() @with_rw_repo("HEAD") def test_push_unsafe_url(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: with self.assertRaises(UnsafeProtocolError): remote.push(url) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_push_unsafe_url_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" remote = rw_repo.remote("origin") urls = [ f"ext::sh -c touch% {tmp_file}", "fd::17/foo", ] for url in urls: # The URL will be allowed into the command, but the command will fail # since we don't have that protocol enabled in the Git config file. with self.assertRaises(GitCommandError): remote.push(url, allow_unsafe_protocols=True) assert not tmp_file.exists() @with_rw_repo("HEAD") def test_push_unsafe_options(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [ {"receive-pack": f"touch {tmp_file}"}, {"receive_pack": f"touch {tmp_file}"}, {"receive_p": f"touch {tmp_file}"}, {"exec": f"touch {tmp_file}"}, {"exe": f"touch {tmp_file}"}, ] for unsafe_option in unsafe_options: assert not tmp_file.exists() with self.assertRaises(UnsafeOptionError): remote.push(**unsafe_option) assert not tmp_file.exists() @pytest.mark.xfail( sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_push_unsafe_options must be adjusted in the " "same way. Until then, test_push_unsafe_options is unreliable on Windows." ), raises=AssertionError, ) @with_rw_repo("HEAD") def test_push_unsafe_options_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) remote = rw_repo.remote("origin") tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [ {"receive-pack": f"touch {tmp_file}"}, {"receive_pack": f"touch {tmp_file}"}, {"exec": f"touch {tmp_file}"}, ] for unsafe_option in unsafe_options: # The options will be allowed, but the command will fail. assert not tmp_file.exists() with self.assertRaises(GitCommandError): remote.push(**unsafe_option, allow_unsafe_options=True) assert tmp_file.exists() tmp_file.unlink() @with_rw_repo("HEAD") def test_ls_remote_unsafe_options(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [ {"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}, {"upl": f"touch {tmp_file}"}, {"exec": f"touch {tmp_file}"}, ] for unsafe_option in unsafe_options: with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote(".", **unsafe_option) with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote([f"--upload-pack={tmp_file}"], ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote([f"--upl={tmp_file}"], ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote(f"--upload-pack={tmp_file}", ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote(f"--upl={tmp_file}", ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote(f"--exec={tmp_file}", ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote("--upload-pack", "touch", ".") with self.assertRaises(UnsafeOptionError): rw_repo.git.ls_remote("--refs", ".", upl=f"touch {tmp_file}") def test_ls_remote_allows_operand_named_like_unsafe_option(self): with mock.patch.object(Git, "_call_process") as git: Git().ls_remote("upload-pack") git.assert_called_once() @with_rw_repo("HEAD") def test_ls_remote_unsafe_options_allowed(self, rw_repo): with writable_fixture("empty.sh") as temporary: tdir = str(temporary.path().resolve()) tmp_dir = Path(tdir) tmp_file = tmp_dir / "pwn" unsafe_options = [{"upload-pack": f"touch {tmp_file}"}] for unsafe_option in unsafe_options: with self.assertRaises(GitCommandError): rw_repo.git.ls_remote(".", **unsafe_option, allow_unsafe_options=True) @pytest.mark.net @with_rw_and_rw_remote_repo("release") def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo): # Create branch with a name containing a NBSP bad_branch_name = f"branch_with_{chr(160)}_nbsp" Head.create(remote_repo, bad_branch_name) # Fetch and get branches remote = rw_repo.remote("origin") branches = remote.fetch() # Test for truncated branch name in branches assert f"origin/{bad_branch_name}" in [b.name for b in branches] # Cleanup branch Head.delete(remote_repo, bad_branch_name) @pytest.mark.net @pytest.mark.skipif(sys.platform == "win32", reason="kill_after_timeout is not supported on Windows") @pytest.mark.parametrize("protocol", ["git", "http", "https"]) @pytest.mark.parametrize("operation", ["fetch", "pull", "push"]) def test_timeout_stalled_remote(protocol, operation): with fixture_repo("remote-timeout.sh", write=True) as repo: with socket.socket() as server: server.bind(("127.0.0.1", 0)) server.listen() server.settimeout(10) finished = threading.Event() connected = threading.Event() def stall(): with server.accept()[0]: connected.set() # Release the connection even if the timeout cleanup deadlocks. finished.wait(5) thread = threading.Thread(target=stall) thread.start() remote = repo.create_remote("origin", f"{protocol}://127.0.0.1:{server.getsockname()[1]}/stalled.git") started = time.monotonic() try: with pytest.raises(GitCommandError, match="process killed because it timed out"): getattr(remote, operation)("main", kill_after_timeout=0.5) assert connected.is_set(), "the command did not reach the stalled remote" assert time.monotonic() - started < 3, "timeout cleanup blocked on a pipe reader" finally: finished.set() thread.join(10) repo.close() assert not thread.is_alive() class TestTimeouts(TestBase): @skipIf(sys.platform == "win32", "requires POSIX timeout signalling") @with_rw_repo("HEAD", bare=False) def test_timeout_partial_push(self, repo): process = repo.git.execute( [ sys.executable, "-c", "import time; print('=\\trefs/heads/main:refs/heads/main\\t[up to date]', flush=True); time.sleep(30)", ], as_process=True, universal_newlines=True, ) with mock.patch("git.remote._logger.warning") as warning: result = repo.remote("origin")._get_push_info(process, None, kill_after_timeout=0.5) warning.assert_not_called() assert len(result) == 1 with pytest.raises(GitCommandError, match="process killed because it timed out"): result.raise_if_error() @with_rw_repo("HEAD", bare=False) def test_timeout_funcs(self, repo): # Maintenance may outlive a timed-out fetch and race with fixture cleanup. with repo.config_writer() as config: config.set_value("maintenance", "auto", False) config.set_value("gc", "auto", 0) # Older Git versions use auto-gc. # Force error code to prevent a race condition if the python thread is slow. with mock.patch.object(Git.AutoInterrupt, "_status_code_if_terminate", -15): for function in ["pull", "fetch"]: # Can't get push to time out. f = getattr(repo.remotes.origin, function) assert f is not None # Make sure these functions exist. _ = f() # Make sure the function runs. with pytest.raises(GitCommandError, match="kill_after_timeout=0 s"): f(kill_after_timeout=0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.