12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007 |
- # -*- coding: utf-8 -*-
- """
- Tests for the Git state
- """
- # Import python libs
- from __future__ import absolute_import, print_function, unicode_literals
- import functools
- import inspect
- import os
- import shutil
- import socket
- import string
- import tempfile
- import pytest
- # Import salt libs
- import salt.utils.files
- import salt.utils.path
- from salt.ext.six.moves.urllib.parse import ( # pylint: disable=no-name-in-module
- urlparse,
- )
- from salt.utils.versions import LooseVersion as _LooseVersion
- from tests.support.case import ModuleCase
- from tests.support.helpers import with_tempdir
- from tests.support.mixins import SaltReturnAssertsMixin
- # Import Salt Testing libs
- from tests.support.runtests import RUNTIME_VARS
- TEST_REPO = "https://github.com/saltstack/salt-test-repo.git"
- def __check_git_version(caller, min_version, skip_msg):
- """
- Common logic for version check
- """
- if inspect.isclass(caller):
- actual_setup = getattr(caller, "setUp", None)
- def setUp(self, *args, **kwargs):
- if not salt.utils.path.which("git"):
- self.skipTest("git is not installed")
- git_version = self.run_function("git.version")
- if _LooseVersion(git_version) < _LooseVersion(min_version):
- self.skipTest(skip_msg.format(min_version, git_version))
- if actual_setup is not None:
- actual_setup(self, *args, **kwargs)
- caller.setUp = setUp
- return caller
- @functools.wraps(caller)
- def wrapper(self, *args, **kwargs):
- if not salt.utils.path.which("git"):
- self.skipTest("git is not installed")
- git_version = self.run_function("git.version")
- if _LooseVersion(git_version) < _LooseVersion(min_version):
- self.skipTest(skip_msg.format(min_version, git_version))
- return caller(self, *args, **kwargs)
- return wrapper
- def ensure_min_git(caller=None, min_version="1.6.5"):
- """
- Skip test if minimum supported git version is not installed
- """
- if caller is None:
- return functools.partial(ensure_min_git, min_version=min_version)
- return __check_git_version(
- caller, min_version, "git {0} or newer required to run this test (detected {1})"
- )
- def uses_git_opts(caller):
- """
- Skip test if git_opts is not supported
- IMPORTANT! This decorator should be at the bottom of any decorators added
- to a given function.
- """
- min_version = "1.7.2"
- return __check_git_version(
- caller,
- min_version,
- "git_opts only supported in git {0} and newer (detected {1})",
- )
- class WithGitMirror(object):
- def __init__(self, repo_url, **kwargs):
- self.repo_url = repo_url
- if "dir" not in kwargs:
- kwargs["dir"] = RUNTIME_VARS.TMP
- self.kwargs = kwargs
- def __call__(self, func):
- self.func = func
- return functools.wraps(func)(
- # pylint: disable=unnecessary-lambda
- lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
- # pylint: enable=unnecessary-lambda
- )
- def wrap(self, testcase, *args, **kwargs):
- # Get temp dir paths
- mirror_dir = tempfile.mkdtemp(**self.kwargs)
- admin_dir = tempfile.mkdtemp(**self.kwargs)
- clone_dir = tempfile.mkdtemp(**self.kwargs)
- # Clean up the directories, we want git to actually create them
- os.rmdir(mirror_dir)
- os.rmdir(admin_dir)
- os.rmdir(clone_dir)
- # Create a URL to clone
- mirror_url = "file://" + mirror_dir
- # Mirror the repo
- testcase.run_function("git.clone", [mirror_dir], url=TEST_REPO, opts="--mirror")
- # Make sure the directory for the mirror now exists
- assert os.path.exists(mirror_dir)
- # Clone to the admin dir
- ret = testcase.run_state("git.latest", name=mirror_url, target=admin_dir)
- ret = ret[next(iter(ret))]
- assert os.path.exists(admin_dir)
- try:
- # Run the actual function with three arguments added:
- # 1. URL for the test to use to clone
- # 2. Cloned admin dir for making/pushing changes to the mirror
- # 3. Yet-nonexistant clone_dir for the test function to use as a
- # destination for cloning.
- return self.func(
- testcase, mirror_url, admin_dir, clone_dir, *args, **kwargs
- )
- finally:
- shutil.rmtree(mirror_dir, ignore_errors=True)
- shutil.rmtree(admin_dir, ignore_errors=True)
- shutil.rmtree(clone_dir, ignore_errors=True)
- with_git_mirror = WithGitMirror
- @ensure_min_git
- class GitTest(ModuleCase, SaltReturnAssertsMixin):
- """
- Validate the git state
- """
- def setUp(self):
- domain = urlparse(TEST_REPO).netloc
- try:
- if hasattr(socket, "setdefaulttimeout"):
- # 10 second dns timeout
- socket.setdefaulttimeout(10)
- socket.gethostbyname(domain)
- except socket.error:
- msg = "error resolving {0}, possible network issue?"
- self.skipTest(msg.format(domain))
- def tearDown(self):
- # Reset the dns timeout after the test is over
- socket.setdefaulttimeout(None)
- def _head(self, cwd):
- return self.run_function("git.rev_parse", [cwd, "HEAD"])
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest(self, target):
- """
- git.latest
- """
- ret = self.run_state("git.latest", name=TEST_REPO, target=target)
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_with_rev_and_submodules(self, target):
- """
- git.latest
- """
- ret = self.run_state(
- "git.latest", name=TEST_REPO, rev="develop", target=target, submodules=True
- )
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_failure(self, target):
- """
- git.latest
- """
- ret = self.run_state(
- "git.latest",
- name="https://youSpelledGitHubWrong.com/saltstack/salt-test-repo.git",
- rev="develop",
- target=target,
- submodules=True,
- )
- self.assertSaltFalseReturn(ret)
- self.assertFalse(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir()
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_empty_dir(self, target):
- """
- git.latest
- """
- ret = self.run_state(
- "git.latest", name=TEST_REPO, rev="develop", target=target, submodules=True
- )
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_unless_no_cwd_issue_6800(self, target):
- """
- cwd=target was being passed to _run_check which blew up if
- target dir did not already exist.
- """
- ret = self.run_state(
- "git.latest",
- name=TEST_REPO,
- rev="develop",
- target=target,
- unless="test -e {0}".format(target),
- submodules=True,
- )
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_numeric_rev(self, target):
- """
- git.latest with numeric revision
- """
- ret = self.run_state(
- "git.latest",
- name=TEST_REPO,
- rev=0.11,
- target=target,
- submodules=True,
- timeout=120,
- )
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_with_local_changes(self, target):
- """
- Ensure that we fail the state when there are local changes and succeed
- when force_reset is True.
- """
- # Clone repo
- ret = self.run_state("git.latest", name=TEST_REPO, target=target)
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- # Make change to LICENSE file.
- with salt.utils.files.fopen(os.path.join(target, "LICENSE"), "a") as fp_:
- fp_.write("Lorem ipsum dolor blah blah blah....\n")
- # Make sure that we now have uncommitted changes
- self.assertTrue(self.run_function("git.diff", [target, "HEAD"]))
- # Re-run state with force_reset=False
- ret = self.run_state(
- "git.latest", name=TEST_REPO, target=target, force_reset=False
- )
- self.assertSaltTrueReturn(ret)
- self.assertEqual(
- ret[next(iter(ret))]["comment"],
- (
- "Repository {0} is up-to-date, but with uncommitted changes. "
- "Set 'force_reset' to True to purge uncommitted changes.".format(target)
- ),
- )
- # Now run the state with force_reset=True
- ret = self.run_state(
- "git.latest", name=TEST_REPO, target=target, force_reset=True
- )
- self.assertSaltTrueReturn(ret)
- # Make sure that we no longer have uncommitted changes
- self.assertFalse(self.run_function("git.diff", [target, "HEAD"]))
- @with_git_mirror(TEST_REPO)
- @uses_git_opts
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_fast_forward(self, mirror_url, admin_dir, clone_dir):
- """
- Test running git.latest state a second time after changes have been
- made to the remote repo.
- """
- # Clone the repo
- ret = self.run_state("git.latest", name=mirror_url, target=clone_dir)
- ret = ret[next(iter(ret))]
- assert ret["result"]
- # Make a change to the repo by editing the file in the admin copy
- # of the repo and committing.
- head_pre = self._head(admin_dir)
- with salt.utils.files.fopen(os.path.join(admin_dir, "LICENSE"), "a") as fp_:
- fp_.write("Hello world!")
- self.run_function(
- "git.commit",
- [admin_dir, "added a line"],
- git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
- opts="-a",
- )
- # Make sure HEAD is pointing to a new SHA so we know we properly
- # committed our change.
- head_post = self._head(admin_dir)
- assert head_pre != head_post
- # Push the change to the mirror
- # NOTE: the test will fail if the salt-test-repo's default branch
- # is changed.
- self.run_function("git.push", [admin_dir, "origin", "develop"])
- # Re-run the git.latest state on the clone_dir
- ret = self.run_state("git.latest", name=mirror_url, target=clone_dir)
- ret = ret[next(iter(ret))]
- assert ret["result"]
- # Make sure that the clone_dir now has the correct SHA
- assert head_post == self._head(clone_dir)
- @with_tempdir(create=False)
- def _changed_local_branch_helper(self, target, rev, hint):
- """
- We're testing two almost identical cases, the only thing that differs
- is the rev used for the git.latest state.
- """
- # Clone repo
- ret = self.run_state("git.latest", name=TEST_REPO, rev=rev, target=target)
- self.assertSaltTrueReturn(ret)
- # Check out a new branch in the clone and make a commit, to ensure
- # that when we re-run the state, it is not a fast-forward change
- self.run_function("git.checkout", [target, "new_branch"], opts="-b")
- with salt.utils.files.fopen(os.path.join(target, "foo"), "w"):
- pass
- self.run_function("git.add", [target, "."])
- self.run_function(
- "git.commit",
- [target, "add file"],
- git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
- )
- # Re-run the state, this should fail with a specific hint in the
- # comment field.
- ret = self.run_state("git.latest", name=TEST_REPO, rev=rev, target=target)
- self.assertSaltFalseReturn(ret)
- comment = ret[next(iter(ret))]["comment"]
- self.assertTrue(hint in comment)
- @uses_git_opts
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_changed_local_branch_rev_head(self):
- """
- Test for presence of hint in failure message when the local branch has
- been changed and a the rev is set to HEAD
- This test will fail if the default branch for the salt-test-repo is
- ever changed.
- """
- self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
- "HEAD",
- "The default remote branch (develop) differs from the local "
- "branch (new_branch)",
- )
- @uses_git_opts
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_changed_local_branch_rev_develop(self):
- """
- Test for presence of hint in failure message when the local branch has
- been changed and a non-HEAD rev is specified
- """
- self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
- "develop",
- "The desired rev (develop) differs from the name of the local "
- "branch (new_branch)",
- )
- @uses_git_opts
- @with_tempdir(create=False)
- @with_tempdir()
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_updated_remote_rev(self, name, target):
- """
- Ensure that we don't exit early when checking for a fast-forward
- """
- # Initialize a new git repository
- self.run_function("git.init", [name])
- # Add and commit a file
- with salt.utils.files.fopen(os.path.join(name, "foo.txt"), "w") as fp_:
- fp_.write("Hello world\n")
- self.run_function("git.add", [name, "."])
- self.run_function(
- "git.commit",
- [name, "initial commit"],
- git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
- )
- # Run the state to clone the repo we just created
- ret = self.run_state("git.latest", name=name, target=target,)
- self.assertSaltTrueReturn(ret)
- # Add another commit
- with salt.utils.files.fopen(os.path.join(name, "foo.txt"), "w") as fp_:
- fp_.write("Added a line\n")
- self.run_function(
- "git.commit",
- [name, "added a line"],
- git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
- opts="-a",
- )
- # Run the state again. It should pass, if it doesn't then there was
- # a problem checking whether or not the change is a fast-forward.
- ret = self.run_state("git.latest", name=name, target=target,)
- self.assertSaltTrueReturn(ret)
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_latest_depth(self, target):
- """
- Test running git.latest state using the "depth" argument to limit the
- history. See #45394.
- """
- ret = self.run_state(
- "git.latest", name=TEST_REPO, rev="HEAD", target=target, depth=1
- )
- # HEAD is not a branch, this should fail
- self.assertSaltFalseReturn(ret)
- self.assertIn(
- "must be set to the name of a branch", ret[next(iter(ret))]["comment"]
- )
- ret = self.run_state(
- "git.latest",
- name=TEST_REPO,
- rev="non-default-branch",
- target=target,
- depth=1,
- )
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isdir(os.path.join(target, ".git")))
- @with_git_mirror(TEST_REPO)
- @uses_git_opts
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_sync_tags(self, mirror_url, admin_dir, clone_dir):
- """
- Test that a removed tag is properly reported as such and removed in the
- local clone, and that new tags are reported as new.
- """
- tag1 = "mytag1"
- tag2 = "mytag2"
- # Add and push a tag
- self.run_function("git.tag", [admin_dir, tag1])
- self.run_function("git.push", [admin_dir, "origin", tag1])
- # Clone the repo
- ret = self.run_state("git.latest", name=mirror_url, target=clone_dir)
- ret = ret[next(iter(ret))]
- assert ret["result"]
- # Now remove the tag
- self.run_function("git.push", [admin_dir, "origin", ":{0}".format(tag1)])
- # Add and push another tag
- self.run_function("git.tag", [admin_dir, tag2])
- self.run_function("git.push", [admin_dir, "origin", tag2])
- # Re-run the state with sync_tags=False. This should NOT delete the tag
- # from the local clone, but should report that a tag has been added.
- ret = self.run_state(
- "git.latest", name=mirror_url, target=clone_dir, sync_tags=False
- )
- ret = ret[next(iter(ret))]
- assert ret["result"]
- # Make ABSOLUTELY SURE both tags are present, since we shouldn't have
- # removed tag1.
- all_tags = self.run_function("git.list_tags", [clone_dir])
- assert tag1 in all_tags
- assert tag2 in all_tags
- # Make sure the reported changes are correct
- expected_changes = {"new_tags": [tag2]}
- assert ret["changes"] == expected_changes, ret["changes"]
- # Re-run the state with sync_tags=True. This should remove the local
- # tag, since it doesn't exist in the remote repository.
- ret = self.run_state(
- "git.latest", name=mirror_url, target=clone_dir, sync_tags=True
- )
- ret = ret[next(iter(ret))]
- assert ret["result"]
- # Make ABSOLUTELY SURE the expected tags are present/gone
- all_tags = self.run_function("git.list_tags", [clone_dir])
- assert tag1 not in all_tags
- assert tag2 in all_tags
- # Make sure the reported changes are correct
- expected_changes = {"deleted_tags": [tag1]}
- assert ret["changes"] == expected_changes, ret["changes"]
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_cloned(self, target):
- """
- Test git.cloned state
- """
- # Test mode
- ret = self.run_state("git.cloned", name=TEST_REPO, target=target, test=True)
- ret = ret[next(iter(ret))]
- assert ret["result"] is None
- assert ret["changes"] == {"new": "{0} => {1}".format(TEST_REPO, target)}
- assert ret["comment"] == "{0} would be cloned to {1}".format(TEST_REPO, target)
- # Now actually run the state
- ret = self.run_state("git.cloned", name=TEST_REPO, target=target)
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert ret["changes"] == {"new": "{0} => {1}".format(TEST_REPO, target)}
- assert ret["comment"] == "{0} cloned to {1}".format(TEST_REPO, target)
- # Run the state again to test idempotence
- ret = self.run_state("git.cloned", name=TEST_REPO, target=target)
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert not ret["changes"]
- assert ret["comment"] == "Repository already exists at {0}".format(target)
- # Run the state again to test idempotence (test mode)
- ret = self.run_state("git.cloned", name=TEST_REPO, target=target, test=True)
- ret = ret[next(iter(ret))]
- assert not ret["changes"]
- assert ret["result"] is True
- assert ret["comment"] == "Repository already exists at {0}".format(target)
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_cloned_with_branch(self, target):
- """
- Test git.cloned state with branch provided
- """
- old_branch = "master"
- new_branch = "develop"
- bad_branch = "thisbranchdoesnotexist"
- # Test mode
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=old_branch, test=True
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is None
- assert ret["changes"] == {"new": "{0} => {1}".format(TEST_REPO, target)}
- assert ret["comment"] == (
- "{0} would be cloned to {1} with branch '{2}'".format(
- TEST_REPO, target, old_branch
- )
- )
- # Now actually run the state
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=old_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert ret["changes"] == {"new": "{0} => {1}".format(TEST_REPO, target)}
- assert ret["comment"] == (
- "{0} cloned to {1} with branch '{2}'".format(TEST_REPO, target, old_branch)
- )
- # Run the state again to test idempotence
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=old_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert not ret["changes"]
- assert ret["comment"] == (
- "Repository already exists at {0} "
- "and is checked out to branch '{1}'".format(target, old_branch)
- )
- # Run the state again to test idempotence (test mode)
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, test=True, branch=old_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert not ret["changes"]
- assert ret["comment"] == (
- "Repository already exists at {0} "
- "and is checked out to branch '{1}'".format(target, old_branch)
- )
- # Change branch (test mode)
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=new_branch, test=True
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is None
- assert ret["changes"] == {"branch": {"old": old_branch, "new": new_branch}}
- assert ret["comment"] == "Branch would be changed to '{0}'".format(new_branch)
- # Now really change the branch
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=new_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert ret["changes"] == {"branch": {"old": old_branch, "new": new_branch}}
- assert ret["comment"] == "Branch changed to '{0}'".format(new_branch)
- # Change back to original branch. This tests that we don't attempt to
- # checkout a new branch (i.e. git checkout -b) for a branch that exists
- # locally, as that would fail.
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=old_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is True
- assert ret["changes"] == {"branch": {"old": new_branch, "new": old_branch}}
- assert ret["comment"] == "Branch changed to '{0}'".format(old_branch)
- # Test switching to a nonexistant branch. This should fail.
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=bad_branch
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is False
- assert not ret["changes"]
- assert ret["comment"].startswith(
- "Failed to change branch to '{0}':".format(bad_branch)
- )
- @with_tempdir(create=False)
- @ensure_min_git(min_version="1.7.10")
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_cloned_with_nonexistant_branch(self, target):
- """
- Test git.cloned state with a nonexistant branch provided
- """
- branch = "thisbranchdoesnotexist"
- # Test mode
- ret = self.run_state(
- "git.cloned", name=TEST_REPO, target=target, branch=branch, test=True
- )
- ret = ret[next(iter(ret))]
- assert ret["result"] is None
- assert ret["changes"]
- assert ret["comment"] == (
- "{0} would be cloned to {1} with branch '{2}'".format(
- TEST_REPO, target, branch
- )
- )
- # Now actually run the state
- ret = self.run_state("git.cloned", name=TEST_REPO, target=target, branch=branch)
- ret = ret[next(iter(ret))]
- assert ret["result"] is False
- assert not ret["changes"]
- assert ret["comment"].startswith("Clone failed:")
- assert "not found in upstream origin" in ret["comment"]
- @with_tempdir(create=False)
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_present(self, name):
- """
- git.present
- """
- ret = self.run_state("git.present", name=name, bare=True)
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isfile(os.path.join(name, "HEAD")))
- @with_tempdir()
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_present_failure(self, name):
- """
- git.present
- """
- fname = os.path.join(name, "stoptheprocess")
- with salt.utils.files.fopen(fname, "a"):
- pass
- ret = self.run_state("git.present", name=name, bare=True)
- self.assertSaltFalseReturn(ret)
- self.assertFalse(os.path.isfile(os.path.join(name, "HEAD")))
- @with_tempdir()
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_present_empty_dir(self, name):
- """
- git.present
- """
- ret = self.run_state("git.present", name=name, bare=True)
- self.assertSaltTrueReturn(ret)
- self.assertTrue(os.path.isfile(os.path.join(name, "HEAD")))
- @with_tempdir()
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_config_set_value_with_space_character(self, name):
- """
- git.config
- """
- self.run_function("git.init", [name])
- ret = self.run_state(
- "git.config_set",
- name="user.name",
- value="foo bar",
- repo=name,
- **{"global": False}
- )
- self.assertSaltTrueReturn(ret)
- @ensure_min_git
- @uses_git_opts
- class LocalRepoGitTest(ModuleCase, SaltReturnAssertsMixin):
- """
- Tests which do no require connectivity to github.com
- """
- def setUp(self):
- self.repo = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
- self.admin = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
- self.target = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
- for dirname in (self.repo, self.admin, self.target):
- self.addCleanup(shutil.rmtree, dirname, ignore_errors=True)
- # Create bare repo
- self.run_function("git.init", [self.repo], bare=True)
- # Clone bare repo
- self.run_function("git.clone", [self.admin], url=self.repo)
- self._commit(self.admin, "", message="initial commit")
- self._push(self.admin)
- def _commit(self, repo_path, content, message):
- with salt.utils.files.fopen(os.path.join(repo_path, "foo"), "a") as fp_:
- fp_.write(content)
- self.run_function("git.add", [repo_path, "."])
- self.run_function(
- "git.commit",
- [repo_path, message],
- git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
- )
- def _push(self, repo_path, remote="origin", ref="master"):
- self.run_function("git.push", [repo_path], remote=remote, ref=ref)
- def _test_latest_force_reset_setup(self):
- # Perform the initial clone
- ret = self.run_state("git.latest", name=self.repo, target=self.target)
- self.assertSaltTrueReturn(ret)
- # Make and push changes to remote repo
- self._commit(self.admin, content="Hello world!\n", message="added a line")
- self._push(self.admin)
- # Make local changes to clone, but don't commit them
- with salt.utils.files.fopen(os.path.join(self.target, "foo"), "a") as fp_:
- fp_.write("Local changes!\n")
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_force_reset_remote_changes(self):
- """
- This tests that an otherwise fast-forward change with local chanegs
- will not reset local changes when force_reset='remote_changes'
- """
- self._test_latest_force_reset_setup()
- # This should fail because of the local changes
- ret = self.run_state("git.latest", name=self.repo, target=self.target)
- self.assertSaltFalseReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("there are uncommitted changes", ret["comment"])
- self.assertIn("Set 'force_reset' to True (or 'remote-changes')", ret["comment"])
- self.assertEqual(ret["changes"], {})
- # Now run again with force_reset='remote_changes', the state should
- # succeed and discard the local changes
- ret = self.run_state(
- "git.latest",
- name=self.repo,
- target=self.target,
- force_reset="remote-changes",
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("Uncommitted changes were discarded", ret["comment"])
- self.assertIn("Repository was fast-forwarded", ret["comment"])
- self.assertNotIn("forced update", ret["changes"])
- self.assertIn("revision", ret["changes"])
- # Add new local changes, but don't commit them
- with salt.utils.files.fopen(os.path.join(self.target, "foo"), "a") as fp_:
- fp_.write("More local changes!\n")
- # Now run again with force_reset='remote_changes', the state should
- # succeed with an up-to-date message and mention that there are local
- # changes, telling the user how to discard them.
- ret = self.run_state(
- "git.latest",
- name=self.repo,
- target=self.target,
- force_reset="remote-changes",
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("up-to-date, but with uncommitted changes", ret["comment"])
- self.assertIn(
- "Set 'force_reset' to True to purge uncommitted changes", ret["comment"]
- )
- self.assertEqual(ret["changes"], {})
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_force_reset_true_fast_forward(self):
- """
- This tests that an otherwise fast-forward change with local chanegs
- does reset local changes when force_reset=True
- """
- self._test_latest_force_reset_setup()
- # Test that local changes are discarded and that we fast-forward
- ret = self.run_state(
- "git.latest", name=self.repo, target=self.target, force_reset=True
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("Uncommitted changes were discarded", ret["comment"])
- self.assertIn("Repository was fast-forwarded", ret["comment"])
- # Add new local changes
- with salt.utils.files.fopen(os.path.join(self.target, "foo"), "a") as fp_:
- fp_.write("More local changes!\n")
- # Running without setting force_reset should mention uncommitted changes
- ret = self.run_state("git.latest", name=self.repo, target=self.target)
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("up-to-date, but with uncommitted changes", ret["comment"])
- self.assertIn(
- "Set 'force_reset' to True to purge uncommitted changes", ret["comment"]
- )
- self.assertEqual(ret["changes"], {})
- # Test that local changes are discarded
- ret = self.run_state(
- "git.latest", name=TEST_REPO, target=self.target, force_reset=True
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- assert "Uncommitted changes were discarded" in ret["comment"]
- assert "Repository was hard-reset" in ret["comment"]
- assert "forced update" in ret["changes"]
- @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
- def test_latest_force_reset_true_non_fast_forward(self):
- """
- This tests that a non fast-forward change with divergent commits fails
- unless force_reset=True.
- """
- self._test_latest_force_reset_setup()
- # Reset to remote HEAD
- ret = self.run_state(
- "git.latest", name=self.repo, target=self.target, force_reset=True
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("Uncommitted changes were discarded", ret["comment"])
- self.assertIn("Repository was fast-forwarded", ret["comment"])
- # Make and push changes to remote repo
- self._commit(self.admin, content="New line\n", message="added another line")
- self._push(self.admin)
- # Make different changes to local file and commit locally
- self._commit(
- self.target,
- content="Different new line\n",
- message="added a different line",
- )
- # This should fail since the local clone has diverged and cannot
- # fast-forward to the remote rev
- ret = self.run_state("git.latest", name=self.repo, target=self.target)
- self.assertSaltFalseReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("this is not a fast-forward merge", ret["comment"])
- self.assertIn("Set 'force_reset' to True to force this update", ret["comment"])
- self.assertEqual(ret["changes"], {})
- # Repeat the state with force_reset=True and confirm that the hard
- # reset was performed
- ret = self.run_state(
- "git.latest", name=self.repo, target=self.target, force_reset=True
- )
- self.assertSaltTrueReturn(ret)
- ret = ret[next(iter(ret))]
- self.assertIn("Repository was hard-reset", ret["comment"])
- self.assertIn("forced update", ret["changes"])
- self.assertIn("revision", ret["changes"])
- @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
- def test_renamed_default_branch(self):
- """
- Test the case where the remote branch has been removed
- https://github.com/saltstack/salt/issues/36242
- """
- # Rename remote 'master' branch to 'develop'
- os.rename(
- os.path.join(self.repo, "refs", "heads", "master"),
- os.path.join(self.repo, "refs", "heads", "develop"),
- )
- # Run git.latest state. This should successfully clone and fail with a
- # specific error in the comment field.
- ret = self.run_state(
- "git.latest", name=self.repo, target=self.target, rev="develop",
- )
- self.assertSaltFalseReturn(ret)
- self.assertEqual(
- ret[next(iter(ret))]["comment"],
- "Remote HEAD refers to a ref that does not exist. "
- "This can happen when the default branch on the "
- "remote repository is renamed or deleted. If you "
- "are unable to fix the remote repository, you can "
- "work around this by setting the 'branch' argument "
- "(which will ensure that the named branch is created "
- "if it does not already exist).\n\n"
- "Changes already made: {0} cloned to {1}".format(self.repo, self.target),
- )
- self.assertEqual(
- ret[next(iter(ret))]["changes"],
- {"new": "{0} => {1}".format(self.repo, self.target)},
- )
- # Run git.latest state again. This should fail again, with a different
- # error in the comment field, and should not change anything.
- ret = self.run_state(
- "git.latest", name=self.repo, target=self.target, rev="develop",
- )
- self.assertSaltFalseReturn(ret)
- self.assertEqual(
- ret[next(iter(ret))]["comment"],
- "Cannot set/unset upstream tracking branch, local "
- "HEAD refers to nonexistent branch. This may have "
- "been caused by cloning a remote repository for which "
- "the default branch was renamed or deleted. If you "
- "are unable to fix the remote repository, you can "
- "work around this by setting the 'branch' argument "
- "(which will ensure that the named branch is created "
- "if it does not already exist).",
- )
- self.assertEqual(ret[next(iter(ret))]["changes"], {})
- # Run git.latest state again with a branch manually set. This should
- # checkout a new branch and the state should pass.
- ret = self.run_state(
- "git.latest",
- name=self.repo,
- target=self.target,
- rev="develop",
- branch="develop",
- )
- # State should succeed
- self.assertSaltTrueReturn(ret)
- self.assertSaltCommentRegexpMatches(
- ret,
- "New branch 'develop' was checked out, with origin/develop "
- r"\([0-9a-f]{7}\) as a starting point",
- )
- # Only the revision should be in the changes dict.
- self.assertEqual(list(ret[next(iter(ret))]["changes"].keys()), ["revision"])
- # Since the remote repo was incorrectly set up, the local head should
- # not exist (therefore the old revision should be None).
- self.assertEqual(ret[next(iter(ret))]["changes"]["revision"]["old"], None)
- # Make sure the new revision is a SHA (40 chars, all hex)
- self.assertTrue(len(ret[next(iter(ret))]["changes"]["revision"]["new"]) == 40)
- self.assertTrue(
- all(
- [
- x in string.hexdigits
- for x in ret[next(iter(ret))]["changes"]["revision"]["new"]
- ]
- )
- )
|