1
0

test_git.py 37 KB

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