# test_remote.py # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import random import tempfile from unittest import skipIf from git import ( RemoteProgress, FetchInfo, Reference, SymbolicReference, Head, Commit, PushInfo, RemoteReference, TagReference, Remote, GitCommandError ) from git.cmd import Git from git.compat import string_types from git.test.lib import ( TestBase, with_rw_repo, with_rw_and_rw_remote_repo, fixture, GIT_DAEMON_PORT, assert_raises ) from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS import os.path as osp # assure we have repeatable results random.seed(0) class TestRemoteProgress(RemoteProgress): __slots__ = ("_seen_lines", "_stages_per_op", '_num_progress_messages') def __init__(self): super(TestRemoteProgress, self).__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(TestRemoteProgress, self)._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 class TestRemote(TestBase): def tearDown(self): import gc 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, string_types) 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.assertGreater(len(results), 0) self.assertIsInstance(results[0], PushInfo) for info in results: self.assertTrue(info.flags) self.assertIsInstance(info.summary, string_types) 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 def _do_test_fetch_info(self, repo): self.failUnlessRaises(ValueError, FetchInfo._from_line, repo, "nonsense", '') self.failUnlessRaises( 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 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): # 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) # 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.failUnlessRaises(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.failUnlessRaises(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 # assure 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: rmtree(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 # assure 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.failUnlessRaises(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.failUnlessRaises(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", message="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, ref='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) # push force 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') # cleanup - delete created tags and branches as we are in an innerloop on # the same repository TagReference.delete(rw_repo, new_tag, other_tag) remote.push(":%s" % other_tag.path) @skipIf(HIDE_WINDOWS_FREEZE_ERRORS, "FIXME: Freezes!") @with_rw_and_rw_remote_repo('0.1.6') 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.failUnlessRaises(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) 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 # 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.failUnlessRaises(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): # assure we can handle remote-tracking branches fetch_info_line_fmt = "c437ee5deb8d00cf02f03720693e4c802e99f390 not-for-merge %s '0.3' of " fetch_info_line_fmt += "git://github.com/gitpython-developers/GitPython" remote_info_line_fmt = "* [new branch] nomatter -> %s" self.failUnlessRaises(ValueError, FetchInfo._from_line, self.rorepo, remote_info_line_fmt % "refs/something/branch", "269c498e56feb93e408ed4558c8138d750de8893\t\t/Users/ben/test/foo\n") fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % "local/master", fetch_info_line_fmt % 'remote-tracking branch') assert not fi.ref.is_valid() self.assertEqual(fi.ref.name, "local/master") # handles non-default refspecs: One can specify a different path in refs/remotes # or a special path just in refs/something for instance fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % "subdir/tagname", fetch_info_line_fmt % 'tag') self.assertIsInstance(fi.ref, TagReference) assert fi.ref.path.startswith('refs/tags'), fi.ref.path # it could be in a remote direcftory though fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % "remotename/tags/tagname", fetch_info_line_fmt % 'tag') self.assertIsInstance(fi.ref, TagReference) assert fi.ref.path.startswith('refs/remotes/'), fi.ref.path # it can also be anywhere ! tag_path = "refs/something/remotename/tags/tagname" fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % tag_path, fetch_info_line_fmt % 'tag') self.assertIsInstance(fi.ref, TagReference) self.assertEqual(fi.ref.path, tag_path) # branches default to refs/remotes fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % "remotename/branch", fetch_info_line_fmt % 'branch') self.assertIsInstance(fi.ref, RemoteReference) self.assertEqual(fi.ref.remote_name, 'remotename') # but you can force it anywhere, in which case we only have a references fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % "refs/something/branch", fetch_info_line_fmt % 'branch') assert type(fi.ref) is Reference, type(fi.ref) self.assertEqual(fi.ref.path, "refs/something/branch") def test_uncommon_branch_names(self): stderr_lines = fixture('uncommon_branch_prefix_stderr').decode('ascii').splitlines() fetch_lines = fixture('uncommon_branch_prefix_FETCH_HEAD').decode('ascii').splitlines() # The contents of the files above must be fetched with a custom refspec: # +refs/pull/*:refs/heads/pull/* res = [FetchInfo._from_line('ShouldntMatterRepo', stderr, fetch_line) for stderr, fetch_line in zip(stderr_lines, fetch_lines)] self.assertGreater(len(res), 0) self.assertEqual(res[0].remote_ref_path, 'refs/pull/1/head') self.assertEqual(res[0].ref.path, 'refs/heads/pull/1/head') self.assertIsInstance(res[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] # Testing setting a single URL remote.set_url(test1) self.assertEqual(list(remote.urls), [test1]) # Testing replacing that single URL remote.set_url(test1) self.assertEqual(list(remote.urls), [test1]) # Testing 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]) # Testing removing an URL remote.set_url(test2, delete=True) self.assertEqual(list(remote.urls), [test1, test3]) # Testing changing an URL remote.set_url(test2, test3) self.assertEqual(list(remote.urls), [test1, test2]) # will raise: fatal: --add --delete doesn't make sense assert_raises(GitCommandError, remote.set_url, test2, add=True, delete=True) # Testing 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]) # Testing 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 assert_raises(GitCommandError, remote.delete_url, test3) def test_fetch_error(self): rem = self.rorepo.remote('origin') with self.assertRaisesRegex(GitCommandError, "[Cc]ouldn't find remote ref __BAD_REF__"): rem.fetch('__BAD_REF__') @with_rw_repo('0.1.6', 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__')