test_git.py 39 KB

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