1
0

test_git.py 38 KB

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