git.latest: fail gracefully for misconfigured remote repo (#36391)

* git.latest: fail gracefully for misconfigured remote repo

When the remote repo's HEAD refers to a nonexistent ref, this was
causing a traceback when we tried to check if the upstream tracking
branch needed to be changed after cloning the repo. This commit fixes
this traceback by gracefully failing the state when the remote HEAD is
not present in the ``git ls-remote`` output, but the desired remote
revision doesn't exist.

Additionally, a similar graceful failure now happens if the state is run
again after we gracefully fail the first time, and we need to set the
tracking branch. Trying to set the tracking branch when there is no
local branch would fail with an ambiguous error like "fatal: branch
'master' does not exist", so before we even attempt to set the tracking
branch, the state is failed with a more descriptive comment.

* Add integration test for #36242
This commit is contained in:
Erik Johnson 2016-09-19 10:33:00 -05:00 committed by Nicole Thomas
parent ad7045ad3b
commit bb4d69f58a
2 changed files with 160 additions and 0 deletions

View file

@ -1083,6 +1083,20 @@ def latest(name,
else:
branch_opts = None
if branch_opts is not None and local_branch is None:
return _fail(
ret,
'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).',
comments
)
if not has_remote_rev:
try:
fetch_changes = __salt__['git.fetch'](
@ -1482,6 +1496,21 @@ def latest(name,
local_rev, local_branch = \
_get_local_rev_and_branch(target, user)
if local_branch is None \
and remote_rev is not None \
and 'HEAD' not in all_remote_refs:
return _fail(
ret,
'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).',
comments
)
if not _revs_equal(local_rev, remote_rev, remote_rev_type):
__salt__['git.reset'](
target,

View file

@ -8,6 +8,7 @@ from __future__ import absolute_import
import os
import shutil
import socket
import string
import subprocess
import tempfile
@ -328,6 +329,136 @@ class GitTest(integration.ModuleCase, integration.SaltReturnAssertsMixIn):
self.assertSaltTrueReturn(ret)
@skip_if_binaries_missing('git')
class LocalRepoGitTest(integration.ModuleCase, integration.SaltReturnAssertsMixIn):
'''
Tests which do no require connectivity to github.com
'''
def test_renamed_default_branch(self):
'''
Test the case where the remote branch has been removed
https://github.com/saltstack/salt/issues/36242
'''
cwd = os.getcwd()
repo = tempfile.mkdtemp(dir=integration.TMP)
admin = tempfile.mkdtemp(dir=integration.TMP)
name = tempfile.mkdtemp(dir=integration.TMP)
for dirname in (repo, admin, name):
self.addCleanup(shutil.rmtree, dirname, ignore_errors=True)
self.addCleanup(os.chdir, cwd)
with salt.utils.fopen(os.devnull, 'w') as devnull:
# Create bare repo
subprocess.check_call(['git', 'init', '--bare', repo],
stdout=devnull, stderr=devnull)
# Clone bare repo
subprocess.check_call(['git', 'clone', repo, admin],
stdout=devnull, stderr=devnull)
# Create, add, commit, and push file
os.chdir(admin)
with salt.utils.fopen('foo', 'w'):
pass
subprocess.check_call(['git', 'add', '.'],
stdout=devnull, stderr=devnull)
subprocess.check_call(['git', 'commit', '-m', 'init'],
stdout=devnull, stderr=devnull)
subprocess.check_call(['git', 'push', 'origin', 'master'],
stdout=devnull, stderr=devnull)
# Change back to the original cwd
os.chdir(cwd)
# Rename remote 'master' branch to 'develop'
os.rename(
os.path.join(repo, 'refs', 'heads', 'master'),
os.path.join(repo, 'refs', 'heads', 'develop')
)
# Run git.latest state. This should successfuly clone and fail with a
# specific error in the comment field.
ret = self.run_state(
'git.latest',
name=repo,
target=name,
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(repo, name)
)
self.assertEqual(
ret[next(iter(ret))]['changes'],
{'new': '{0} => {1}'.format(repo, name)}
)
# 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=repo,
target=name,
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=repo,
target=name,
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']])
)
if __name__ == '__main__':
from integration import run_tests
run_tests(GitTest)