test_git.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  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.runtime import RUNTIME_VARS
  16. from tests.support.case import ModuleCase
  17. from tests.support.helpers import with_tempdir
  18. from tests.support.mixins import SaltReturnAssertsMixin
  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'] = RUNTIME_VARS.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 uncommitted changes. '
  251. 'Set \'force_reset\' to True to purge uncommitted changes.'
  252. .format(target))
  253. )
  254. # Now run the state with force_reset=True
  255. ret = self.run_state(
  256. 'git.latest',
  257. name=TEST_REPO,
  258. target=target,
  259. force_reset=True
  260. )
  261. self.assertSaltTrueReturn(ret)
  262. # Make sure that we no longer have uncommitted changes
  263. self.assertFalse(self.run_function('git.diff', [target, 'HEAD']))
  264. @with_git_mirror(TEST_REPO)
  265. @uses_git_opts
  266. def test_latest_fast_forward(self, mirror_url, admin_dir, clone_dir):
  267. '''
  268. Test running git.latest state a second time after changes have been
  269. made to the remote repo.
  270. '''
  271. # Clone the repo
  272. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  273. ret = ret[next(iter(ret))]
  274. assert ret['result']
  275. # Make a change to the repo by editing the file in the admin copy
  276. # of the repo and committing.
  277. head_pre = self._head(admin_dir)
  278. with salt.utils.files.fopen(os.path.join(admin_dir, 'LICENSE'), 'a') as fp_:
  279. fp_.write('Hello world!')
  280. self.run_function(
  281. 'git.commit', [admin_dir, 'added a line'],
  282. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  283. opts='-a',
  284. )
  285. # Make sure HEAD is pointing to a new SHA so we know we properly
  286. # committed our change.
  287. head_post = self._head(admin_dir)
  288. assert head_pre != head_post
  289. # Push the change to the mirror
  290. # NOTE: the test will fail if the salt-test-repo's default branch
  291. # is changed.
  292. self.run_function('git.push', [admin_dir, 'origin', 'develop'])
  293. # Re-run the git.latest state on the clone_dir
  294. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  295. ret = ret[next(iter(ret))]
  296. assert ret['result']
  297. # Make sure that the clone_dir now has the correct SHA
  298. assert head_post == self._head(clone_dir)
  299. @with_tempdir(create=False)
  300. def _changed_local_branch_helper(self, target, rev, hint):
  301. '''
  302. We're testing two almost identical cases, the only thing that differs
  303. is the rev used for the git.latest state.
  304. '''
  305. # Clone repo
  306. ret = self.run_state(
  307. 'git.latest',
  308. name=TEST_REPO,
  309. rev=rev,
  310. target=target
  311. )
  312. self.assertSaltTrueReturn(ret)
  313. # Check out a new branch in the clone and make a commit, to ensure
  314. # that when we re-run the state, it is not a fast-forward change
  315. self.run_function('git.checkout', [target, 'new_branch'], opts='-b')
  316. with salt.utils.files.fopen(os.path.join(target, 'foo'), 'w'):
  317. pass
  318. self.run_function('git.add', [target, '.'])
  319. self.run_function(
  320. 'git.commit', [target, 'add file'],
  321. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  322. )
  323. # Re-run the state, this should fail with a specific hint in the
  324. # comment field.
  325. ret = self.run_state(
  326. 'git.latest',
  327. name=TEST_REPO,
  328. rev=rev,
  329. target=target
  330. )
  331. self.assertSaltFalseReturn(ret)
  332. comment = ret[next(iter(ret))]['comment']
  333. self.assertTrue(hint in comment)
  334. @uses_git_opts
  335. def test_latest_changed_local_branch_rev_head(self):
  336. '''
  337. Test for presence of hint in failure message when the local branch has
  338. been changed and a the rev is set to HEAD
  339. This test will fail if the default branch for the salt-test-repo is
  340. ever changed.
  341. '''
  342. self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
  343. 'HEAD',
  344. 'The default remote branch (develop) differs from the local '
  345. 'branch (new_branch)'
  346. )
  347. @uses_git_opts
  348. def test_latest_changed_local_branch_rev_develop(self):
  349. '''
  350. Test for presence of hint in failure message when the local branch has
  351. been changed and a non-HEAD rev is specified
  352. '''
  353. self._changed_local_branch_helper( # pylint: disable=no-value-for-parameter
  354. 'develop',
  355. 'The desired rev (develop) differs from the name of the local '
  356. 'branch (new_branch)'
  357. )
  358. @uses_git_opts
  359. @with_tempdir(create=False)
  360. @with_tempdir()
  361. def test_latest_updated_remote_rev(self, name, target):
  362. '''
  363. Ensure that we don't exit early when checking for a fast-forward
  364. '''
  365. # Initialize a new git repository
  366. self.run_function('git.init', [name])
  367. # Add and commit a file
  368. with salt.utils.files.fopen(os.path.join(name, 'foo.txt'), 'w') as fp_:
  369. fp_.write('Hello world\n')
  370. self.run_function('git.add', [name, '.'])
  371. self.run_function(
  372. 'git.commit', [name, 'initial commit'],
  373. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  374. )
  375. # Run the state to clone the repo we just created
  376. ret = self.run_state(
  377. 'git.latest',
  378. name=name,
  379. target=target,
  380. )
  381. self.assertSaltTrueReturn(ret)
  382. # Add another commit
  383. with salt.utils.files.fopen(os.path.join(name, 'foo.txt'), 'w') as fp_:
  384. fp_.write('Added a line\n')
  385. self.run_function(
  386. 'git.commit', [name, 'added a line'],
  387. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  388. opts='-a',
  389. )
  390. # Run the state again. It should pass, if it doesn't then there was
  391. # a problem checking whether or not the change is a fast-forward.
  392. ret = self.run_state(
  393. 'git.latest',
  394. name=name,
  395. target=target,
  396. )
  397. self.assertSaltTrueReturn(ret)
  398. @with_tempdir(create=False)
  399. def test_latest_depth(self, target):
  400. '''
  401. Test running git.latest state using the "depth" argument to limit the
  402. history. See #45394.
  403. '''
  404. ret = self.run_state(
  405. 'git.latest',
  406. name=TEST_REPO,
  407. rev='HEAD',
  408. target=target,
  409. depth=1
  410. )
  411. # HEAD is not a branch, this should fail
  412. self.assertSaltFalseReturn(ret)
  413. self.assertIn(
  414. 'must be set to the name of a branch',
  415. ret[next(iter(ret))]['comment']
  416. )
  417. ret = self.run_state(
  418. 'git.latest',
  419. name=TEST_REPO,
  420. rev='non-default-branch',
  421. target=target,
  422. depth=1
  423. )
  424. self.assertSaltTrueReturn(ret)
  425. self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
  426. @with_git_mirror(TEST_REPO)
  427. @uses_git_opts
  428. def test_latest_sync_tags(self, mirror_url, admin_dir, clone_dir):
  429. '''
  430. Test that a removed tag is properly reported as such and removed in the
  431. local clone, and that new tags are reported as new.
  432. '''
  433. tag1 = 'mytag1'
  434. tag2 = 'mytag2'
  435. # Add and push a tag
  436. self.run_function('git.tag', [admin_dir, tag1])
  437. self.run_function('git.push', [admin_dir, 'origin', tag1])
  438. # Clone the repo
  439. ret = self.run_state('git.latest', name=mirror_url, target=clone_dir)
  440. ret = ret[next(iter(ret))]
  441. assert ret['result']
  442. # Now remove the tag
  443. self.run_function('git.push', [admin_dir, 'origin', ':{0}'.format(tag1)])
  444. # Add and push another tag
  445. self.run_function('git.tag', [admin_dir, tag2])
  446. self.run_function('git.push', [admin_dir, 'origin', tag2])
  447. # Re-run the state with sync_tags=False. This should NOT delete the tag
  448. # from the local clone, but should report that a tag has been added.
  449. ret = self.run_state('git.latest',
  450. name=mirror_url,
  451. target=clone_dir,
  452. sync_tags=False)
  453. ret = ret[next(iter(ret))]
  454. assert ret['result']
  455. # Make ABSOLUTELY SURE both tags are present, since we shouldn't have
  456. # removed tag1.
  457. all_tags = self.run_function('git.list_tags', [clone_dir])
  458. assert tag1 in all_tags
  459. assert tag2 in all_tags
  460. # Make sure the reported changes are correct
  461. expected_changes = {'new_tags': [tag2]}
  462. assert ret['changes'] == expected_changes, ret['changes']
  463. # Re-run the state with sync_tags=True. This should remove the local
  464. # tag, since it doesn't exist in the remote repository.
  465. ret = self.run_state('git.latest',
  466. name=mirror_url,
  467. target=clone_dir,
  468. sync_tags=True)
  469. ret = ret[next(iter(ret))]
  470. assert ret['result']
  471. # Make ABSOLUTELY SURE the expected tags are present/gone
  472. all_tags = self.run_function('git.list_tags', [clone_dir])
  473. assert tag1 not in all_tags
  474. assert tag2 in all_tags
  475. # Make sure the reported changes are correct
  476. expected_changes = {'deleted_tags': [tag1]}
  477. assert ret['changes'] == expected_changes, ret['changes']
  478. @with_tempdir(create=False)
  479. def test_cloned(self, target):
  480. '''
  481. Test git.cloned state
  482. '''
  483. # Test mode
  484. ret = self.run_state(
  485. 'git.cloned',
  486. name=TEST_REPO,
  487. target=target,
  488. test=True)
  489. ret = ret[next(iter(ret))]
  490. assert ret['result'] is None
  491. assert ret['changes'] == {
  492. 'new': '{0} => {1}'.format(TEST_REPO, target)
  493. }
  494. assert ret['comment'] == '{0} would be cloned to {1}'.format(
  495. TEST_REPO,
  496. target
  497. )
  498. # Now actually run the state
  499. ret = self.run_state(
  500. 'git.cloned',
  501. name=TEST_REPO,
  502. target=target)
  503. ret = ret[next(iter(ret))]
  504. assert ret['result'] is True
  505. assert ret['changes'] == {
  506. 'new': '{0} => {1}'.format(TEST_REPO, target)
  507. }
  508. assert ret['comment'] == '{0} cloned to {1}'.format(TEST_REPO, target)
  509. # Run the state again to test idempotence
  510. ret = self.run_state(
  511. 'git.cloned',
  512. name=TEST_REPO,
  513. target=target)
  514. ret = ret[next(iter(ret))]
  515. assert ret['result'] is True
  516. assert not ret['changes']
  517. assert ret['comment'] == 'Repository already exists at {0}'.format(target)
  518. # Run the state again to test idempotence (test mode)
  519. ret = self.run_state(
  520. 'git.cloned',
  521. name=TEST_REPO,
  522. target=target,
  523. test=True)
  524. ret = ret[next(iter(ret))]
  525. assert not ret['changes']
  526. assert ret['result'] is True
  527. assert ret['comment'] == 'Repository already exists at {0}'.format(target)
  528. @with_tempdir(create=False)
  529. def test_cloned_with_branch(self, target):
  530. '''
  531. Test git.cloned state with branch provided
  532. '''
  533. old_branch = 'master'
  534. new_branch = 'develop'
  535. bad_branch = 'thisbranchdoesnotexist'
  536. # Test mode
  537. ret = self.run_state(
  538. 'git.cloned',
  539. name=TEST_REPO,
  540. target=target,
  541. branch=old_branch,
  542. test=True)
  543. ret = ret[next(iter(ret))]
  544. assert ret['result'] is None
  545. assert ret['changes'] == {
  546. 'new': '{0} => {1}'.format(TEST_REPO, target)
  547. }
  548. assert ret['comment'] == (
  549. '{0} would be cloned to {1} with branch \'{2}\''.format(
  550. TEST_REPO,
  551. target,
  552. old_branch
  553. )
  554. )
  555. # Now actually run the state
  556. ret = self.run_state(
  557. 'git.cloned',
  558. name=TEST_REPO,
  559. target=target,
  560. branch=old_branch)
  561. ret = ret[next(iter(ret))]
  562. assert ret['result'] is True
  563. assert ret['changes'] == {
  564. 'new': '{0} => {1}'.format(TEST_REPO, target)
  565. }
  566. assert ret['comment'] == (
  567. '{0} cloned to {1} with branch \'{2}\''.format(
  568. TEST_REPO,
  569. target,
  570. old_branch
  571. )
  572. )
  573. # Run the state again to test idempotence
  574. ret = self.run_state(
  575. 'git.cloned',
  576. name=TEST_REPO,
  577. target=target,
  578. branch=old_branch)
  579. ret = ret[next(iter(ret))]
  580. assert ret['result'] is True
  581. assert not ret['changes']
  582. assert ret['comment'] == (
  583. 'Repository already exists at {0} '
  584. 'and is checked out to branch \'{1}\''.format(target, old_branch)
  585. )
  586. # Run the state again to test idempotence (test mode)
  587. ret = self.run_state(
  588. 'git.cloned',
  589. name=TEST_REPO,
  590. target=target,
  591. test=True,
  592. branch=old_branch)
  593. ret = ret[next(iter(ret))]
  594. assert ret['result'] is True
  595. assert not ret['changes']
  596. assert ret['comment'] == (
  597. 'Repository already exists at {0} '
  598. 'and is checked out to branch \'{1}\''.format(target, old_branch)
  599. )
  600. # Change branch (test mode)
  601. ret = self.run_state(
  602. 'git.cloned',
  603. name=TEST_REPO,
  604. target=target,
  605. branch=new_branch,
  606. test=True)
  607. ret = ret[next(iter(ret))]
  608. assert ret['result'] is None
  609. assert ret['changes'] == {
  610. 'branch': {'old': old_branch, 'new': new_branch}
  611. }
  612. assert ret['comment'] == 'Branch would be changed to \'{0}\''.format(
  613. new_branch
  614. )
  615. # Now really change the branch
  616. ret = self.run_state(
  617. 'git.cloned',
  618. name=TEST_REPO,
  619. target=target,
  620. branch=new_branch)
  621. ret = ret[next(iter(ret))]
  622. assert ret['result'] is True
  623. assert ret['changes'] == {
  624. 'branch': {'old': old_branch, 'new': new_branch}
  625. }
  626. assert ret['comment'] == 'Branch changed to \'{0}\''.format(
  627. new_branch
  628. )
  629. # Change back to original branch. This tests that we don't attempt to
  630. # checkout a new branch (i.e. git checkout -b) for a branch that exists
  631. # locally, as that would fail.
  632. ret = self.run_state(
  633. 'git.cloned',
  634. name=TEST_REPO,
  635. target=target,
  636. branch=old_branch)
  637. ret = ret[next(iter(ret))]
  638. assert ret['result'] is True
  639. assert ret['changes'] == {
  640. 'branch': {'old': new_branch, 'new': old_branch}
  641. }
  642. assert ret['comment'] == 'Branch changed to \'{0}\''.format(
  643. old_branch
  644. )
  645. # Test switching to a nonexistant branch. This should fail.
  646. ret = self.run_state(
  647. 'git.cloned',
  648. name=TEST_REPO,
  649. target=target,
  650. branch=bad_branch)
  651. ret = ret[next(iter(ret))]
  652. assert ret['result'] is False
  653. assert not ret['changes']
  654. assert ret['comment'].startswith(
  655. 'Failed to change branch to \'{0}\':'.format(bad_branch)
  656. )
  657. @with_tempdir(create=False)
  658. @ensure_min_git(min_version='1.7.10')
  659. def test_cloned_with_nonexistant_branch(self, target):
  660. '''
  661. Test git.cloned state with a nonexistant branch provided
  662. '''
  663. branch = 'thisbranchdoesnotexist'
  664. # Test mode
  665. ret = self.run_state(
  666. 'git.cloned',
  667. name=TEST_REPO,
  668. target=target,
  669. branch=branch,
  670. test=True)
  671. ret = ret[next(iter(ret))]
  672. assert ret['result'] is None
  673. assert ret['changes']
  674. assert ret['comment'] == (
  675. '{0} would be cloned to {1} with branch \'{2}\''.format(
  676. TEST_REPO,
  677. target,
  678. branch
  679. )
  680. )
  681. # Now actually run the state
  682. ret = self.run_state(
  683. 'git.cloned',
  684. name=TEST_REPO,
  685. target=target,
  686. branch=branch)
  687. ret = ret[next(iter(ret))]
  688. assert ret['result'] is False
  689. assert not ret['changes']
  690. assert ret['comment'].startswith('Clone failed:')
  691. assert 'not found in upstream origin' in ret['comment']
  692. @with_tempdir(create=False)
  693. def test_present(self, name):
  694. '''
  695. git.present
  696. '''
  697. ret = self.run_state(
  698. 'git.present',
  699. name=name,
  700. bare=True
  701. )
  702. self.assertSaltTrueReturn(ret)
  703. self.assertTrue(os.path.isfile(os.path.join(name, 'HEAD')))
  704. @with_tempdir()
  705. def test_present_failure(self, name):
  706. '''
  707. git.present
  708. '''
  709. fname = os.path.join(name, 'stoptheprocess')
  710. with salt.utils.files.fopen(fname, 'a'):
  711. pass
  712. ret = self.run_state(
  713. 'git.present',
  714. name=name,
  715. bare=True
  716. )
  717. self.assertSaltFalseReturn(ret)
  718. self.assertFalse(os.path.isfile(os.path.join(name, 'HEAD')))
  719. @with_tempdir()
  720. def test_present_empty_dir(self, name):
  721. '''
  722. git.present
  723. '''
  724. ret = self.run_state(
  725. 'git.present',
  726. name=name,
  727. bare=True
  728. )
  729. self.assertSaltTrueReturn(ret)
  730. self.assertTrue(os.path.isfile(os.path.join(name, 'HEAD')))
  731. @with_tempdir()
  732. def test_config_set_value_with_space_character(self, name):
  733. '''
  734. git.config
  735. '''
  736. self.run_function('git.init', [name])
  737. ret = self.run_state(
  738. 'git.config_set',
  739. name='user.name',
  740. value='foo bar',
  741. repo=name,
  742. **{'global': False})
  743. self.assertSaltTrueReturn(ret)
  744. @ensure_min_git
  745. @uses_git_opts
  746. class LocalRepoGitTest(ModuleCase, SaltReturnAssertsMixin):
  747. '''
  748. Tests which do no require connectivity to github.com
  749. '''
  750. def setUp(self):
  751. self.repo = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  752. self.admin = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  753. self.target = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  754. for dirname in (self.repo, self.admin, self.target):
  755. self.addCleanup(shutil.rmtree, dirname, ignore_errors=True)
  756. # Create bare repo
  757. self.run_function('git.init', [self.repo], bare=True)
  758. # Clone bare repo
  759. self.run_function('git.clone', [self.admin], url=self.repo)
  760. self._commit(self.admin, '', message='initial commit')
  761. self._push(self.admin)
  762. def _commit(self, repo_path, content, message):
  763. with salt.utils.files.fopen(os.path.join(repo_path, 'foo'), 'a') as fp_:
  764. fp_.write(content)
  765. self.run_function('git.add', [repo_path, '.'])
  766. self.run_function(
  767. 'git.commit', [repo_path, message],
  768. git_opts='-c user.name="Foo Bar" -c user.email=foo@bar.com',
  769. )
  770. def _push(self, repo_path, remote='origin', ref='master'):
  771. self.run_function('git.push', [repo_path], remote=remote, ref=ref)
  772. def _test_latest_force_reset_setup(self):
  773. # Perform the initial clone
  774. ret = self.run_state(
  775. 'git.latest',
  776. name=self.repo,
  777. target=self.target)
  778. self.assertSaltTrueReturn(ret)
  779. # Make and push changes to remote repo
  780. self._commit(self.admin,
  781. content='Hello world!\n',
  782. message='added a line')
  783. self._push(self.admin)
  784. # Make local changes to clone, but don't commit them
  785. with salt.utils.files.fopen(os.path.join(self.target, 'foo'), 'a') as fp_:
  786. fp_.write('Local changes!\n')
  787. def test_latest_force_reset_remote_changes(self):
  788. '''
  789. This tests that an otherwise fast-forward change with local chanegs
  790. will not reset local changes when force_reset='remote_changes'
  791. '''
  792. self._test_latest_force_reset_setup()
  793. # This should fail because of the local changes
  794. ret = self.run_state(
  795. 'git.latest',
  796. name=self.repo,
  797. target=self.target)
  798. self.assertSaltFalseReturn(ret)
  799. ret = ret[next(iter(ret))]
  800. self.assertIn('there are uncommitted changes', ret['comment'])
  801. self.assertIn(
  802. 'Set \'force_reset\' to True (or \'remote-changes\')',
  803. ret['comment']
  804. )
  805. self.assertEqual(ret['changes'], {})
  806. # Now run again with force_reset='remote_changes', the state should
  807. # succeed and discard the local changes
  808. ret = self.run_state(
  809. 'git.latest',
  810. name=self.repo,
  811. target=self.target,
  812. force_reset='remote-changes')
  813. self.assertSaltTrueReturn(ret)
  814. ret = ret[next(iter(ret))]
  815. self.assertIn('Uncommitted changes were discarded', ret['comment'])
  816. self.assertIn('Repository was fast-forwarded', ret['comment'])
  817. self.assertNotIn('forced update', ret['changes'])
  818. self.assertIn('revision', ret['changes'])
  819. # Add new local changes, but don't commit them
  820. with salt.utils.files.fopen(os.path.join(self.target, 'foo'), 'a') as fp_:
  821. fp_.write('More local changes!\n')
  822. # Now run again with force_reset='remote_changes', the state should
  823. # succeed with an up-to-date message and mention that there are local
  824. # changes, telling the user how to discard them.
  825. ret = self.run_state(
  826. 'git.latest',
  827. name=self.repo,
  828. target=self.target,
  829. force_reset='remote-changes')
  830. self.assertSaltTrueReturn(ret)
  831. ret = ret[next(iter(ret))]
  832. self.assertIn('up-to-date, but with uncommitted changes', ret['comment'])
  833. self.assertIn(
  834. 'Set \'force_reset\' to True to purge uncommitted changes',
  835. ret['comment']
  836. )
  837. self.assertEqual(ret['changes'], {})
  838. def test_latest_force_reset_true_fast_forward(self):
  839. '''
  840. This tests that an otherwise fast-forward change with local chanegs
  841. does reset local changes when force_reset=True
  842. '''
  843. self._test_latest_force_reset_setup()
  844. # Test that local changes are discarded and that we fast-forward
  845. ret = self.run_state(
  846. 'git.latest',
  847. name=self.repo,
  848. target=self.target,
  849. force_reset=True)
  850. self.assertSaltTrueReturn(ret)
  851. ret = ret[next(iter(ret))]
  852. self.assertIn('Uncommitted changes were discarded', ret['comment'])
  853. self.assertIn('Repository was fast-forwarded', ret['comment'])
  854. # Add new local changes
  855. with salt.utils.files.fopen(os.path.join(self.target, 'foo'), 'a') as fp_:
  856. fp_.write('More local changes!\n')
  857. # Running without setting force_reset should mention uncommitted changes
  858. ret = self.run_state(
  859. 'git.latest',
  860. name=self.repo,
  861. target=self.target)
  862. self.assertSaltTrueReturn(ret)
  863. ret = ret[next(iter(ret))]
  864. self.assertIn('up-to-date, but with uncommitted changes', ret['comment'])
  865. self.assertIn(
  866. 'Set \'force_reset\' to True to purge uncommitted changes',
  867. ret['comment']
  868. )
  869. self.assertEqual(ret['changes'], {})
  870. # Test that local changes are discarded
  871. ret = self.run_state(
  872. 'git.latest',
  873. name=TEST_REPO,
  874. target=self.target,
  875. force_reset=True)
  876. self.assertSaltTrueReturn(ret)
  877. ret = ret[next(iter(ret))]
  878. assert 'Uncommitted changes were discarded' in ret['comment']
  879. assert 'Repository was hard-reset' in ret['comment']
  880. assert 'forced update' in ret['changes']
  881. def test_latest_force_reset_true_non_fast_forward(self):
  882. '''
  883. This tests that a non fast-forward change with divergent commits fails
  884. unless force_reset=True.
  885. '''
  886. self._test_latest_force_reset_setup()
  887. # Reset to remote HEAD
  888. ret = self.run_state(
  889. 'git.latest',
  890. name=self.repo,
  891. target=self.target,
  892. force_reset=True)
  893. self.assertSaltTrueReturn(ret)
  894. ret = ret[next(iter(ret))]
  895. self.assertIn('Uncommitted changes were discarded', ret['comment'])
  896. self.assertIn('Repository was fast-forwarded', ret['comment'])
  897. # Make and push changes to remote repo
  898. self._commit(self.admin,
  899. content='New line\n',
  900. message='added another line')
  901. self._push(self.admin)
  902. # Make different changes to local file and commit locally
  903. self._commit(self.target,
  904. content='Different new line\n',
  905. message='added a different line')
  906. # This should fail since the local clone has diverged and cannot
  907. # fast-forward to the remote rev
  908. ret = self.run_state(
  909. 'git.latest',
  910. name=self.repo,
  911. target=self.target)
  912. self.assertSaltFalseReturn(ret)
  913. ret = ret[next(iter(ret))]
  914. self.assertIn('this is not a fast-forward merge', ret['comment'])
  915. self.assertIn(
  916. 'Set \'force_reset\' to True to force this update',
  917. ret['comment']
  918. )
  919. self.assertEqual(ret['changes'], {})
  920. # Repeat the state with force_reset=True and confirm that the hard
  921. # reset was performed
  922. ret = self.run_state(
  923. 'git.latest',
  924. name=self.repo,
  925. target=self.target,
  926. force_reset=True)
  927. self.assertSaltTrueReturn(ret)
  928. ret = ret[next(iter(ret))]
  929. self.assertIn('Repository was hard-reset', ret['comment'])
  930. self.assertIn('forced update', ret['changes'])
  931. self.assertIn('revision', ret['changes'])
  932. def test_renamed_default_branch(self):
  933. '''
  934. Test the case where the remote branch has been removed
  935. https://github.com/saltstack/salt/issues/36242
  936. '''
  937. # Rename remote 'master' branch to 'develop'
  938. os.rename(
  939. os.path.join(self.repo, 'refs', 'heads', 'master'),
  940. os.path.join(self.repo, 'refs', 'heads', 'develop')
  941. )
  942. # Run git.latest state. This should successfully clone and fail with a
  943. # specific error in the comment field.
  944. ret = self.run_state(
  945. 'git.latest',
  946. name=self.repo,
  947. target=self.target,
  948. rev='develop',
  949. )
  950. self.assertSaltFalseReturn(ret)
  951. self.assertEqual(
  952. ret[next(iter(ret))]['comment'],
  953. 'Remote HEAD refers to a ref that does not exist. '
  954. 'This can happen when the default branch on the '
  955. 'remote repository is renamed or deleted. If you '
  956. 'are unable to fix the remote repository, you can '
  957. 'work around this by setting the \'branch\' argument '
  958. '(which will ensure that the named branch is created '
  959. 'if it does not already exist).\n\n'
  960. 'Changes already made: {0} cloned to {1}'
  961. .format(self.repo, self.target)
  962. )
  963. self.assertEqual(
  964. ret[next(iter(ret))]['changes'],
  965. {'new': '{0} => {1}'.format(self.repo, self.target)}
  966. )
  967. # Run git.latest state again. This should fail again, with a different
  968. # error in the comment field, and should not change anything.
  969. ret = self.run_state(
  970. 'git.latest',
  971. name=self.repo,
  972. target=self.target,
  973. rev='develop',
  974. )
  975. self.assertSaltFalseReturn(ret)
  976. self.assertEqual(
  977. ret[next(iter(ret))]['comment'],
  978. 'Cannot set/unset upstream tracking branch, local '
  979. 'HEAD refers to nonexistent branch. This may have '
  980. 'been caused by cloning a remote repository for which '
  981. 'the default branch was renamed or deleted. If you '
  982. 'are unable to fix the remote repository, you can '
  983. 'work around this by setting the \'branch\' argument '
  984. '(which will ensure that the named branch is created '
  985. 'if it does not already exist).'
  986. )
  987. self.assertEqual(ret[next(iter(ret))]['changes'], {})
  988. # Run git.latest state again with a branch manually set. This should
  989. # checkout a new branch and the state should pass.
  990. ret = self.run_state(
  991. 'git.latest',
  992. name=self.repo,
  993. target=self.target,
  994. rev='develop',
  995. branch='develop',
  996. )
  997. # State should succeed
  998. self.assertSaltTrueReturn(ret)
  999. self.assertSaltCommentRegexpMatches(
  1000. ret,
  1001. 'New branch \'develop\' was checked out, with origin/develop '
  1002. r'\([0-9a-f]{7}\) as a starting point'
  1003. )
  1004. # Only the revision should be in the changes dict.
  1005. self.assertEqual(
  1006. list(ret[next(iter(ret))]['changes'].keys()),
  1007. ['revision']
  1008. )
  1009. # Since the remote repo was incorrectly set up, the local head should
  1010. # not exist (therefore the old revision should be None).
  1011. self.assertEqual(
  1012. ret[next(iter(ret))]['changes']['revision']['old'],
  1013. None
  1014. )
  1015. # Make sure the new revision is a SHA (40 chars, all hex)
  1016. self.assertTrue(
  1017. len(ret[next(iter(ret))]['changes']['revision']['new']) == 40)
  1018. self.assertTrue(
  1019. all([x in string.hexdigits for x in
  1020. ret[next(iter(ret))]['changes']['revision']['new']])
  1021. )