test_git.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Tests for the Git state
  4. '''
  5. # Import python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import functools
  8. import inspect
  9. import os
  10. import shutil
  11. import socket
  12. import string
  13. import tempfile
  14. # Import Salt Testing libs
  15. from tests.support.case import ModuleCase
  16. from tests.support.helpers import with_tempdir
  17. from tests.support.mixins import SaltReturnAssertsMixin
  18. from tests.support.paths import TMP
  19. # Import salt libs
  20. import salt.utils.files
  21. import salt.utils.path
  22. from salt.utils.versions import LooseVersion as _LooseVersion
  23. from salt.ext.six.moves.urllib.parse import urlparse # pylint: disable=no-name-in-module
  24. TEST_REPO = 'https://github.com/saltstack/salt-test-repo.git'
  25. def __check_git_version(caller, min_version, skip_msg):
  26. '''
  27. Common logic for version check
  28. '''
  29. if inspect.isclass(caller):
  30. actual_setup = getattr(caller, 'setUp', None)
  31. def setUp(self, *args, **kwargs):
  32. if not salt.utils.path.which('git'):
  33. self.skipTest('git is not installed')
  34. git_version = self.run_function('git.version')
  35. if _LooseVersion(git_version) < _LooseVersion(min_version):
  36. self.skipTest(skip_msg.format(min_version, git_version))
  37. if actual_setup is not None:
  38. actual_setup(self, *args, **kwargs)
  39. caller.setUp = setUp
  40. return caller
  41. @functools.wraps(caller)
  42. def wrapper(self, *args, **kwargs):
  43. if not salt.utils.path.which('git'):
  44. self.skipTest('git is not installed')
  45. git_version = self.run_function('git.version')
  46. if _LooseVersion(git_version) < _LooseVersion(min_version):
  47. self.skipTest(skip_msg.format(min_version, git_version))
  48. return caller(self, *args, **kwargs)
  49. return wrapper
  50. def ensure_min_git(caller=None, min_version='1.6.5'):
  51. '''
  52. Skip test if minimum supported git version is not installed
  53. '''
  54. if caller is None:
  55. return functools.partial(ensure_min_git, min_version=min_version)
  56. return __check_git_version(
  57. caller,
  58. min_version,
  59. 'git {0} or newer required to run this test (detected {1})'
  60. )
  61. def uses_git_opts(caller):
  62. '''
  63. Skip test if git_opts is not supported
  64. IMPORTANT! This decorator should be at the bottom of any decorators added
  65. to a given function.
  66. '''
  67. min_version = '1.7.2'
  68. return __check_git_version(
  69. caller,
  70. min_version,
  71. 'git_opts only supported in git {0} and newer (detected {1})'
  72. )
  73. class WithGitMirror(object):
  74. def __init__(self, repo_url, **kwargs):
  75. self.repo_url = repo_url
  76. if 'dir' not in kwargs:
  77. kwargs['dir'] = TMP
  78. self.kwargs = kwargs
  79. def __call__(self, func):
  80. self.func = func
  81. return functools.wraps(func)(
  82. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs) # pylint: disable=W0108
  83. )
  84. def wrap(self, testcase, *args, **kwargs):
  85. # Get temp dir paths
  86. mirror_dir = tempfile.mkdtemp(**self.kwargs)
  87. admin_dir = tempfile.mkdtemp(**self.kwargs)
  88. clone_dir = tempfile.mkdtemp(**self.kwargs)
  89. # Clean up the directories, we want git to actually create them
  90. os.rmdir(mirror_dir)
  91. os.rmdir(admin_dir)
  92. os.rmdir(clone_dir)
  93. # Create a URL to clone
  94. mirror_url = 'file://' + mirror_dir
  95. # Mirror the repo
  96. testcase.run_function(
  97. 'git.clone', [mirror_dir], url=TEST_REPO, opts='--mirror')
  98. # Make sure the directory for the mirror now exists
  99. assert os.path.exists(mirror_dir)
  100. # Clone to the admin dir
  101. ret = testcase.run_state('git.latest', name=mirror_url, target=admin_dir)
  102. ret = ret[next(iter(ret))]
  103. assert os.path.exists(admin_dir)
  104. try:
  105. # Run the actual function with three arguments added:
  106. # 1. URL for the test to use to clone
  107. # 2. Cloned admin dir for making/pushing changes to the mirror
  108. # 3. Yet-nonexistant clone_dir for the test function to use as a
  109. # destination for cloning.
  110. return self.func(testcase, mirror_url, admin_dir, clone_dir, *args, **kwargs)
  111. finally:
  112. shutil.rmtree(mirror_dir, ignore_errors=True)
  113. shutil.rmtree(admin_dir, ignore_errors=True)
  114. shutil.rmtree(clone_dir, ignore_errors=True)
  115. with_git_mirror = WithGitMirror
  116. @ensure_min_git
  117. class GitTest(ModuleCase, SaltReturnAssertsMixin):
  118. '''
  119. Validate the git state
  120. '''
  121. def setUp(self):
  122. domain = urlparse(TEST_REPO).netloc
  123. try:
  124. if hasattr(socket, 'setdefaulttimeout'):
  125. # 10 second dns timeout
  126. socket.setdefaulttimeout(10)
  127. socket.gethostbyname(domain)
  128. except socket.error:
  129. msg = 'error resolving {0}, possible network issue?'
  130. self.skipTest(msg.format(domain))
  131. def tearDown(self):
  132. # Reset the dns timeout after the test is over
  133. socket.setdefaulttimeout(None)
  134. def _head(self, cwd):
  135. return self.run_function('git.rev_parse', [cwd, 'HEAD'])
  136. @with_tempdir(create=False)
  137. def test_latest(self, target):
  138. '''
  139. git.latest
  140. '''
  141. ret = self.run_state(
  142. 'git.latest',
  143. name=TEST_REPO,
  144. target=target
  145. )
  146. self.assertSaltTrueReturn(ret)
  147. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  148. @with_tempdir(create=False)
  149. def test_latest_with_rev_and_submodules(self, target):
  150. '''
  151. git.latest
  152. '''
  153. ret = self.run_state(
  154. 'git.latest',
  155. name=TEST_REPO,
  156. rev='develop',
  157. target=target,
  158. submodules=True
  159. )
  160. self.assertSaltTrueReturn(ret)
  161. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  162. @with_tempdir(create=False)
  163. def test_latest_failure(self, target):
  164. '''
  165. git.latest
  166. '''
  167. ret = self.run_state(
  168. 'git.latest',
  169. name='https://youSpelledGitHubWrong.com/saltstack/salt-test-repo.git',
  170. rev='develop',
  171. target=target,
  172. submodules=True
  173. )
  174. self.assertSaltFalseReturn(ret)
  175. self.assertFalse(os.path.isdir(os.path.join(target, '.git')))
  176. @with_tempdir()
  177. def test_latest_empty_dir(self, target):
  178. '''
  179. git.latest
  180. '''
  181. ret = self.run_state(
  182. 'git.latest',
  183. name=TEST_REPO,
  184. rev='develop',
  185. target=target,
  186. submodules=True
  187. )
  188. self.assertSaltTrueReturn(ret)
  189. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  190. @with_tempdir(create=False)
  191. def test_latest_unless_no_cwd_issue_6800(self, target):
  192. '''
  193. cwd=target was being passed to _run_check which blew up if
  194. target dir did not already exist.
  195. '''
  196. ret = self.run_state(
  197. 'git.latest',
  198. name=TEST_REPO,
  199. rev='develop',
  200. target=target,
  201. unless='test -e {0}'.format(target),
  202. submodules=True
  203. )
  204. self.assertSaltTrueReturn(ret)
  205. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  206. @with_tempdir(create=False)
  207. def test_numeric_rev(self, target):
  208. '''
  209. git.latest with numeric revision
  210. '''
  211. ret = self.run_state(
  212. 'git.latest',
  213. name=TEST_REPO,
  214. rev=0.11,
  215. target=target,
  216. submodules=True,
  217. timeout=120
  218. )
  219. self.assertSaltTrueReturn(ret)
  220. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  221. @with_tempdir(create=False)
  222. def test_latest_with_local_changes(self, target):
  223. '''
  224. Ensure that we fail the state when there are local changes and succeed
  225. when force_reset is True.
  226. '''
  227. # Clone repo
  228. ret = self.run_state(
  229. 'git.latest',
  230. name=TEST_REPO,
  231. target=target
  232. )
  233. self.assertSaltTrueReturn(ret)
  234. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  235. # Make change to LICENSE file.
  236. with salt.utils.files.fopen(os.path.join(target, 'LICENSE'), 'a') as fp_:
  237. fp_.write('Lorem ipsum dolor blah blah blah....\n')
  238. # Make sure that we now have uncommitted changes
  239. self.assertTrue(self.run_function('git.diff', [target, 'HEAD']))
  240. # Re-run state with force_reset=False
  241. ret = self.run_state(
  242. 'git.latest',
  243. name=TEST_REPO,
  244. target=target,
  245. force_reset=False
  246. )
  247. self.assertSaltTrueReturn(ret)
  248. self.assertEqual(
  249. ret[next(iter(ret))]['comment'],
  250. ('Repository {0} is up-to-date, but with local changes. Set '
  251. '\'force_reset\' to True to purge local changes.'.format(target))
  252. )
  253. # Now run the state with force_reset=True
  254. ret = self.run_state(
  255. 'git.latest',
  256. name=TEST_REPO,
  257. target=target,
  258. force_reset=True
  259. )
  260. self.assertSaltTrueReturn(ret)
  261. # Make sure that we no longer have uncommitted changes
  262. self.assertFalse(self.run_function('git.diff', [target, 'HEAD']))
  263. @with_git_mirror(TEST_REPO)
  264. @uses_git_opts
  265. def test_latest_fast_forward(self, mirror_url, admin_dir, clone_dir):
  266. '''
  267. Test running git.latest state a second time after changes have been
  268. made to the remote repo.
  269. '''
  270. # Clone the repo
  271. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  272. ret = ret[next(iter(ret))]
  273. assert ret['result']
  274. # Make a change to the repo by editing the file in the admin copy
  275. # of the repo and committing.
  276. head_pre = self._head(admin_dir)
  277. with salt.utils.files.fopen(os.path.join(admin_dir, 'LICENSE'), 'a') as fp_:
  278. fp_.write('Hello world!')
  279. self.run_function(
  280. 'git.commit', [admin_dir, 'added a line'],
  281. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  282. opts='-a',
  283. )
  284. # Make sure HEAD is pointing to a new SHA so we know we properly
  285. # committed our change.
  286. head_post = self._head(admin_dir)
  287. assert head_pre != head_post
  288. # Push the change to the mirror
  289. # NOTE: the test will fail if the salt-test-repo's default branch
  290. # is changed.
  291. self.run_function('git.push', [admin_dir, 'origin', 'develop'])
  292. # Re-run the git.latest state on the clone_dir
  293. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  294. ret = ret[next(iter(ret))]
  295. assert ret['result']
  296. # Make sure that the clone_dir now has the correct SHA
  297. assert head_post == self._head(clone_dir)
  298. @with_tempdir(create=False)
  299. def _changed_local_branch_helper(self, target, rev, hint):
  300. '''
  301. We're testing two almost identical cases, the only thing that differs
  302. is the rev used for the git.latest state.
  303. '''
  304. # Clone repo
  305. ret = self.run_state(
  306. 'git.latest',
  307. name=TEST_REPO,
  308. rev=rev,
  309. target=target
  310. )
  311. self.assertSaltTrueReturn(ret)
  312. # Check out a new branch in the clone and make a commit, to ensure
  313. # that when we re-run the state, it is not a fast-forward change
  314. self.run_function('git.checkout', [target, 'new_branch'], opts='-b')
  315. with salt.utils.files.fopen(os.path.join(target, 'foo'), 'w'):
  316. pass
  317. self.run_function('git.add', [target, '.'])
  318. self.run_function(
  319. 'git.commit', [target, 'add file'],
  320. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  321. )
  322. # Re-run the state, this should fail with a specific hint in the
  323. # comment field.
  324. ret = self.run_state(
  325. 'git.latest',
  326. name=TEST_REPO,
  327. rev=rev,
  328. target=target
  329. )
  330. self.assertSaltFalseReturn(ret)
  331. comment = ret[next(iter(ret))]['comment']
  332. self.assertTrue(hint in comment)
  333. @uses_git_opts
  334. def test_latest_changed_local_branch_rev_head(self):
  335. '''
  336. Test for presence of hint in failure message when the local branch has
  337. been changed and a the rev is set to HEAD
  338. This test will fail if the default branch for the salt-test-repo is
  339. ever changed.
  340. '''
  341. self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
  342. 'HEAD',
  343. 'The default remote branch (develop) differs from the local '
  344. 'branch (new_branch)'
  345. )
  346. @uses_git_opts
  347. def test_latest_changed_local_branch_rev_develop(self):
  348. '''
  349. Test for presence of hint in failure message when the local branch has
  350. been changed and a non-HEAD rev is specified
  351. '''
  352. self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
  353. 'develop',
  354. 'The desired rev (develop) differs from the name of the local '
  355. 'branch (new_branch)'
  356. )
  357. @uses_git_opts
  358. @with_tempdir(create=False)
  359. @with_tempdir()
  360. def test_latest_updated_remote_rev(self, name, target):
  361. '''
  362. Ensure that we don't exit early when checking for a fast-forward
  363. '''
  364. # Initialize a new git repository
  365. self.run_function('git.init', [name])
  366. # Add and commit a file
  367. with salt.utils.files.fopen(os.path.join(name, 'foo.txt'), 'w') as fp_:
  368. fp_.write('Hello world\n')
  369. self.run_function('git.add', [name, '.'])
  370. self.run_function(
  371. 'git.commit', [name, 'initial commit'],
  372. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  373. )
  374. # Run the state to clone the repo we just created
  375. ret = self.run_state(
  376. 'git.latest',
  377. name=name,
  378. target=target,
  379. )
  380. self.assertSaltTrueReturn(ret)
  381. # Add another commit
  382. with salt.utils.files.fopen(os.path.join(name, 'foo.txt'), 'w') as fp_:
  383. fp_.write('Added a line\n')
  384. self.run_function(
  385. 'git.commit', [name, 'added a line'],
  386. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  387. opts='-a',
  388. )
  389. # Run the state again. It should pass, if it doesn't then there was
  390. # a problem checking whether or not the change is a fast-forward.
  391. ret = self.run_state(
  392. 'git.latest',
  393. name=name,
  394. target=target,
  395. )
  396. self.assertSaltTrueReturn(ret)
  397. @with_tempdir(create=False)
  398. def test_latest_depth(self, target):
  399. '''
  400. Test running git.latest state using the "depth" argument to limit the
  401. history. See #45394.
  402. '''
  403. ret = self.run_state(
  404. 'git.latest',
  405. name=TEST_REPO,
  406. rev='HEAD',
  407. target=target,
  408. depth=1
  409. )
  410. # HEAD is not a branch, this should fail
  411. self.assertSaltFalseReturn(ret)
  412. self.assertIn(
  413. 'must be set to the name of a branch',
  414. ret[next(iter(ret))]['comment']
  415. )
  416. ret = self.run_state(
  417. 'git.latest',
  418. name=TEST_REPO,
  419. rev='non-default-branch',
  420. target=target,
  421. depth=1
  422. )
  423. self.assertSaltTrueReturn(ret)
  424. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  425. @with_git_mirror(TEST_REPO)
  426. @uses_git_opts
  427. def test_latest_sync_tags(self, mirror_url, admin_dir, clone_dir):
  428. '''
  429. Test that a removed tag is properly reported as such and removed in the
  430. local clone, and that new tags are reported as new.
  431. '''
  432. tag1 = 'mytag1'
  433. tag2 = 'mytag2'
  434. # Add and push a tag
  435. self.run_function('git.tag', [admin_dir, tag1])
  436. self.run_function('git.push', [admin_dir, 'origin', tag1])
  437. # Clone the repo
  438. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  439. ret = ret[next(iter(ret))]
  440. assert ret['result']
  441. # Now remove the tag
  442. self.run_function('git.push', [admin_dir, 'origin', ':{0}'.format(tag1)])
  443. # Add and push another tag
  444. self.run_function('git.tag', [admin_dir, tag2])
  445. self.run_function('git.push', [admin_dir, 'origin', tag2])
  446. # Re-run the state with sync_tags=False. This should NOT delete the tag
  447. # from the local clone, but should report that a tag has been added.
  448. ret = self.run_state('git.latest',
  449. name=mirror_url,
  450. target=clone_dir,
  451. sync_tags=False)
  452. ret = ret[next(iter(ret))]
  453. assert ret['result']
  454. # Make ABSOLUTELY SURE both tags are present, since we shouldn't have
  455. # removed tag1.
  456. all_tags = self.run_function('git.list_tags', [clone_dir])
  457. assert tag1 in all_tags
  458. assert tag2 in all_tags
  459. # Make sure the reported changes are correct
  460. expected_changes = {'new_tags': [tag2]}
  461. assert ret['changes'] == expected_changes, ret['changes']
  462. # Re-run the state with sync_tags=True. This should remove the local
  463. # tag, since it doesn't exist in the remote repository.
  464. ret = self.run_state('git.latest',
  465. name=mirror_url,
  466. target=clone_dir,
  467. sync_tags=True)
  468. ret = ret[next(iter(ret))]
  469. assert ret['result']
  470. # Make ABSOLUTELY SURE the expected tags are present/gone
  471. all_tags = self.run_function('git.list_tags', [clone_dir])
  472. assert tag1 not in all_tags
  473. assert tag2 in all_tags
  474. # Make sure the reported changes are correct
  475. expected_changes = {'deleted_tags': [tag1]}
  476. assert ret['changes'] == expected_changes, ret['changes']
  477. @with_tempdir(create=False)
  478. def test_cloned(self, target):
  479. '''
  480. Test git.cloned state
  481. '''
  482. # Test mode
  483. ret = self.run_state(
  484. 'git.cloned',
  485. name=TEST_REPO,
  486. target=target,
  487. test=True)
  488. ret = ret[next(iter(ret))]
  489. assert ret['result'] is None
  490. assert ret['changes'] == {
  491. 'new': '{0} => {1}'.format(TEST_REPO, target)
  492. }
  493. assert ret['comment'] == '{0} would be cloned to {1}'.format(
  494. TEST_REPO,
  495. target
  496. )
  497. # Now actually run the state
  498. ret = self.run_state(
  499. 'git.cloned',
  500. name=TEST_REPO,
  501. target=target)
  502. ret = ret[next(iter(ret))]
  503. assert ret['result'] is True
  504. assert ret['changes'] == {
  505. 'new': '{0} => {1}'.format(TEST_REPO, target)
  506. }
  507. assert ret['comment'] == '{0} cloned to {1}'.format(TEST_REPO, target)
  508. # Run the state again to test idempotence
  509. ret = self.run_state(
  510. 'git.cloned',
  511. name=TEST_REPO,
  512. target=target)
  513. ret = ret[next(iter(ret))]
  514. assert ret['result'] is True
  515. assert not ret['changes']
  516. assert ret['comment'] == 'Repository already exists at {0}'.format(target)
  517. # Run the state again to test idempotence (test mode)
  518. ret = self.run_state(
  519. 'git.cloned',
  520. name=TEST_REPO,
  521. target=target,
  522. test=True)
  523. ret = ret[next(iter(ret))]
  524. assert not ret['changes']
  525. assert ret['result'] is True
  526. assert ret['comment'] == 'Repository already exists at {0}'.format(target)
  527. @with_tempdir(create=False)
  528. def test_cloned_with_branch(self, target):
  529. '''
  530. Test git.cloned state with branch provided
  531. '''
  532. old_branch = 'master'
  533. new_branch = 'develop'
  534. bad_branch = 'thisbranchdoesnotexist'
  535. # Test mode
  536. ret = self.run_state(
  537. 'git.cloned',
  538. name=TEST_REPO,
  539. target=target,
  540. branch=old_branch,
  541. test=True)
  542. ret = ret[next(iter(ret))]
  543. assert ret['result'] is None
  544. assert ret['changes'] == {
  545. 'new': '{0} => {1}'.format(TEST_REPO, target)
  546. }
  547. assert ret['comment'] == (
  548. '{0} would be cloned to {1} with branch \'{2}\''.format(
  549. TEST_REPO,
  550. target,
  551. old_branch
  552. )
  553. )
  554. # Now actually run the state
  555. ret = self.run_state(
  556. 'git.cloned',
  557. name=TEST_REPO,
  558. target=target,
  559. branch=old_branch)
  560. ret = ret[next(iter(ret))]
  561. assert ret['result'] is True
  562. assert ret['changes'] == {
  563. 'new': '{0} => {1}'.format(TEST_REPO, target)
  564. }
  565. assert ret['comment'] == (
  566. '{0} cloned to {1} with branch \'{2}\''.format(
  567. TEST_REPO,
  568. target,
  569. old_branch
  570. )
  571. )
  572. # Run the state again to test idempotence
  573. ret = self.run_state(
  574. 'git.cloned',
  575. name=TEST_REPO,
  576. target=target,
  577. branch=old_branch)
  578. ret = ret[next(iter(ret))]
  579. assert ret['result'] is True
  580. assert not ret['changes']
  581. assert ret['comment'] == (
  582. 'Repository already exists at {0} '
  583. 'and is checked out to branch \'{1}\''.format(target, old_branch)
  584. )
  585. # Run the state again to test idempotence (test mode)
  586. ret = self.run_state(
  587. 'git.cloned',
  588. name=TEST_REPO,
  589. target=target,
  590. test=True,
  591. branch=old_branch)
  592. ret = ret[next(iter(ret))]
  593. assert ret['result'] is True
  594. assert not ret['changes']
  595. assert ret['comment'] == (
  596. 'Repository already exists at {0} '
  597. 'and is checked out to branch \'{1}\''.format(target, old_branch)
  598. )
  599. # Change branch (test mode)
  600. ret = self.run_state(
  601. 'git.cloned',
  602. name=TEST_REPO,
  603. target=target,
  604. branch=new_branch,
  605. test=True)
  606. ret = ret[next(iter(ret))]
  607. assert ret['result'] is None
  608. assert ret['changes'] == {
  609. 'branch': {'old': old_branch, 'new': new_branch}
  610. }
  611. assert ret['comment'] == 'Branch would be changed to \'{0}\''.format(
  612. new_branch
  613. )
  614. # Now really change the branch
  615. ret = self.run_state(
  616. 'git.cloned',
  617. name=TEST_REPO,
  618. target=target,
  619. branch=new_branch)
  620. ret = ret[next(iter(ret))]
  621. assert ret['result'] is True
  622. assert ret['changes'] == {
  623. 'branch': {'old': old_branch, 'new': new_branch}
  624. }
  625. assert ret['comment'] == 'Branch changed to \'{0}\''.format(
  626. new_branch
  627. )
  628. # Change back to original branch. This tests that we don't attempt to
  629. # checkout a new branch (i.e. git checkout -b) for a branch that exists
  630. # locally, as that would fail.
  631. ret = self.run_state(
  632. 'git.cloned',
  633. name=TEST_REPO,
  634. target=target,
  635. branch=old_branch)
  636. ret = ret[next(iter(ret))]
  637. assert ret['result'] is True
  638. assert ret['changes'] == {
  639. 'branch': {'old': new_branch, 'new': old_branch}
  640. }
  641. assert ret['comment'] == 'Branch changed to \'{0}\''.format(
  642. old_branch
  643. )
  644. # Test switching to a nonexistant branch. This should fail.
  645. ret = self.run_state(
  646. 'git.cloned',
  647. name=TEST_REPO,
  648. target=target,
  649. branch=bad_branch)
  650. ret = ret[next(iter(ret))]
  651. assert ret['result'] is False
  652. assert not ret['changes']
  653. assert ret['comment'].startswith(
  654. 'Failed to change branch to \'{0}\':'.format(bad_branch)
  655. )
  656. @with_tempdir(create=False)
  657. @ensure_min_git(min_version='1.7.10')
  658. def test_cloned_with_nonexistant_branch(self, target):
  659. '''
  660. Test git.cloned state with a nonexistant branch provided
  661. '''
  662. branch = 'thisbranchdoesnotexist'
  663. # Test mode
  664. ret = self.run_state(
  665. 'git.cloned',
  666. name=TEST_REPO,
  667. target=target,
  668. branch=branch,
  669. test=True)
  670. ret = ret[next(iter(ret))]
  671. assert ret['result'] is None
  672. assert ret['changes']
  673. assert ret['comment'] == (
  674. '{0} would be cloned to {1} with branch \'{2}\''.format(
  675. TEST_REPO,
  676. target,
  677. branch
  678. )
  679. )
  680. # Now actually run the state
  681. ret = self.run_state(
  682. 'git.cloned',
  683. name=TEST_REPO,
  684. target=target,
  685. branch=branch)
  686. ret = ret[next(iter(ret))]
  687. assert ret['result'] is False
  688. assert not ret['changes']
  689. assert ret['comment'].startswith('Clone failed:')
  690. assert 'not found in upstream origin' in ret['comment']
  691. @with_tempdir(create=False)
  692. def test_present(self, name):
  693. '''
  694. git.present
  695. '''
  696. ret = self.run_state(
  697. 'git.present',
  698. name=name,
  699. bare=True
  700. )
  701. self.assertSaltTrueReturn(ret)
  702. self.assertTrue(os.path.isfile(os.path.join(name, 'HEAD')))
  703. @with_tempdir()
  704. def test_present_failure(self, name):
  705. '''
  706. git.present
  707. '''
  708. fname = os.path.join(name, 'stoptheprocess')
  709. with salt.utils.files.fopen(fname, 'a'):
  710. pass
  711. ret = self.run_state(
  712. 'git.present',
  713. name=name,
  714. bare=True
  715. )
  716. self.assertSaltFalseReturn(ret)
  717. self.assertFalse(os.path.isfile(os.path.join(name, 'HEAD')))
  718. @with_tempdir()
  719. def test_present_empty_dir(self, name):
  720. '''
  721. git.present
  722. '''
  723. ret = self.run_state(
  724. 'git.present',
  725. name=name,
  726. bare=True
  727. )
  728. self.assertSaltTrueReturn(ret)
  729. self.assertTrue(os.path.isfile(os.path.join(name, 'HEAD')))
  730. @with_tempdir()
  731. def test_config_set_value_with_space_character(self, name):
  732. '''
  733. git.config
  734. '''
  735. self.run_function('git.init', [name])
  736. ret = self.run_state(
  737. 'git.config_set',
  738. name='user.name',
  739. value='foo bar',
  740. repo=name,
  741. **{'global': False})
  742. self.assertSaltTrueReturn(ret)
  743. @ensure_min_git
  744. @uses_git_opts
  745. class LocalRepoGitTest(ModuleCase, SaltReturnAssertsMixin):
  746. '''
  747. Tests which do no require connectivity to github.com
  748. '''
  749. @with_tempdir()
  750. @with_tempdir()
  751. @with_tempdir()
  752. def test_renamed_default_branch(self, repo, admin, target):
  753. '''
  754. Test the case where the remote branch has been removed
  755. https://github.com/saltstack/salt/issues/36242
  756. '''
  757. # Create bare repo
  758. self.run_function('git.init', [repo], bare=True)
  759. # Clone bare repo
  760. self.run_function('git.clone', [admin], url=repo)
  761. # Create, add, commit, and push file
  762. with salt.utils.files.fopen(os.path.join(admin, 'foo'), 'w'):
  763. pass
  764. self.run_function('git.add', [admin, '.'])
  765. self.run_function(
  766. 'git.commit', [admin, 'initial commit'],
  767. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  768. )
  769. self.run_function('git.push', [admin], remote='origin', ref='master')
  770. # Rename remote 'master' branch to 'develop'
  771. os.rename(
  772. os.path.join(repo, 'refs', 'heads', 'master'),
  773. os.path.join(repo, 'refs', 'heads', 'develop')
  774. )
  775. # Run git.latest state. This should successfully clone and fail with a
  776. # specific error in the comment field.
  777. ret = self.run_state(
  778. 'git.latest',
  779. name=repo,
  780. target=target,
  781. rev='develop',
  782. )
  783. self.assertSaltFalseReturn(ret)
  784. self.assertEqual(
  785. ret[next(iter(ret))]['comment'],
  786. 'Remote HEAD refers to a ref that does not exist. '
  787. 'This can happen when the default branch on the '
  788. 'remote repository is renamed or deleted. If you '
  789. 'are unable to fix the remote repository, you can '
  790. 'work around this by setting the \'branch\' argument '
  791. '(which will ensure that the named branch is created '
  792. 'if it does not already exist).\n\n'
  793. 'Changes already made: {0} cloned to {1}'
  794. .format(repo, target)
  795. )
  796. self.assertEqual(
  797. ret[next(iter(ret))]['changes'],
  798. {'new': '{0} => {1}'.format(repo, target)}
  799. )
  800. # Run git.latest state again. This should fail again, with a different
  801. # error in the comment field, and should not change anything.
  802. ret = self.run_state(
  803. 'git.latest',
  804. name=repo,
  805. target=target,
  806. rev='develop',
  807. )
  808. self.assertSaltFalseReturn(ret)
  809. self.assertEqual(
  810. ret[next(iter(ret))]['comment'],
  811. 'Cannot set/unset upstream tracking branch, local '
  812. 'HEAD refers to nonexistent branch. This may have '
  813. 'been caused by cloning a remote repository for which '
  814. 'the default branch was renamed or deleted. If you '
  815. 'are unable to fix the remote repository, you can '
  816. 'work around this by setting the \'branch\' argument '
  817. '(which will ensure that the named branch is created '
  818. 'if it does not already exist).'
  819. )
  820. self.assertEqual(ret[next(iter(ret))]['changes'], {})
  821. # Run git.latest state again with a branch manually set. This should
  822. # checkout a new branch and the state should pass.
  823. ret = self.run_state(
  824. 'git.latest',
  825. name=repo,
  826. target=target,
  827. rev='develop',
  828. branch='develop',
  829. )
  830. # State should succeed
  831. self.assertSaltTrueReturn(ret)
  832. self.assertSaltCommentRegexpMatches(
  833. ret,
  834. 'New branch \'develop\' was checked out, with origin/develop '
  835. r'\([0-9a-f]{7}\) as a starting point'
  836. )
  837. # Only the revision should be in the changes dict.
  838. self.assertEqual(
  839. list(ret[next(iter(ret))]['changes'].keys()),
  840. ['revision']
  841. )
  842. # Since the remote repo was incorrectly set up, the local head should
  843. # not exist (therefore the old revision should be None).
  844. self.assertEqual(
  845. ret[next(iter(ret))]['changes']['revision']['old'],
  846. None
  847. )
  848. # Make sure the new revision is a SHA (40 chars, all hex)
  849. self.assertTrue(
  850. len(ret[next(iter(ret))]['changes']['revision']['new']) == 40)
  851. self.assertTrue(
  852. all([x in string.hexdigits for x in
  853. ret[next(iter(ret))]['changes']['revision']['new']])
  854. )