test_git.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests for git execution module
  4. NOTE: These tests may modify the global git config, and have been marked as
  5. destructive as a result. If no values are set for user.name or user.email in
  6. the user's global .gitconfig, then these tests will set one.
  7. """
  8. from __future__ import absolute_import, print_function, unicode_literals
  9. import errno
  10. import logging
  11. import os
  12. import re
  13. import shutil
  14. import subprocess
  15. import tarfile
  16. import tempfile
  17. from contextlib import closing
  18. import pytest
  19. import salt.utils.data
  20. import salt.utils.files
  21. import salt.utils.platform
  22. from salt.ext import six
  23. from salt.utils.versions import LooseVersion
  24. from tests.support.case import ModuleCase
  25. from tests.support.runtests import RUNTIME_VARS
  26. from tests.support.unit import skipIf
  27. log = logging.getLogger(__name__)
  28. def _git_version():
  29. try:
  30. git_version = subprocess.Popen(
  31. ["git", "--version"],
  32. shell=False,
  33. close_fds=False if salt.utils.platform.is_windows() else True,
  34. stdout=subprocess.PIPE,
  35. stderr=subprocess.PIPE,
  36. ).communicate()[0]
  37. except OSError:
  38. return False
  39. if not git_version:
  40. log.debug("Git not installed")
  41. return False
  42. git_version = git_version.strip().split()[-1]
  43. if six.PY3:
  44. git_version = git_version.decode(__salt_system_encoding__)
  45. log.debug("Detected git version: %s", git_version)
  46. return LooseVersion(git_version)
  47. def _worktrees_supported():
  48. """
  49. Check if the git version is 2.5.0 or later
  50. """
  51. try:
  52. return _git_version() >= LooseVersion("2.5.0")
  53. except AttributeError:
  54. return False
  55. def _makedirs(path):
  56. try:
  57. os.makedirs(path)
  58. except OSError as exc:
  59. # Don't raise an exception if the directory exists
  60. if exc.errno != errno.EEXIST:
  61. raise
  62. @pytest.mark.windows_whitelisted
  63. @pytest.mark.skip_if_binaries_missing("git")
  64. class GitModuleTest(ModuleCase):
  65. def setUp(self):
  66. super(GitModuleTest, self).setUp()
  67. self.orig_cwd = os.getcwd()
  68. self.addCleanup(os.chdir, self.orig_cwd)
  69. self.repo = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  70. self.addCleanup(shutil.rmtree, self.repo, ignore_errors=True)
  71. self.files = ("foo", "bar", "baz", "питон")
  72. self.dirs = ("", "qux")
  73. self.branches = ("master", "iamanewbranch")
  74. self.tags = ("git_testing",)
  75. for dirname in self.dirs:
  76. dir_path = os.path.join(self.repo, dirname)
  77. _makedirs(dir_path)
  78. for filename in self.files:
  79. with salt.utils.files.fopen(
  80. os.path.join(dir_path, filename), "wb"
  81. ) as fp_:
  82. fp_.write(
  83. "This is a test file named {0}.".format(filename).encode(
  84. "utf-8"
  85. )
  86. )
  87. # Navigate to the root of the repo to init, stage, and commit
  88. os.chdir(self.repo)
  89. # Initialize a new git repository
  90. subprocess.check_call(["git", "init", "--quiet", self.repo])
  91. # Set user.name and user.email config attributes if not present
  92. for key, value in (
  93. ("user.name", "Jenkins"),
  94. ("user.email", "qa@saltstack.com"),
  95. ):
  96. # Check if key is missing
  97. keycheck = subprocess.Popen(
  98. ["git", "config", "--get", "--global", key],
  99. stdout=subprocess.PIPE,
  100. stderr=subprocess.PIPE,
  101. )
  102. if keycheck.wait() != 0:
  103. # Set the key if it is not present
  104. subprocess.check_call(["git", "config", "--global", key, value])
  105. subprocess.check_call(["git", "add", "."])
  106. subprocess.check_call(
  107. ["git", "commit", "--quiet", "--message", "Initial commit"]
  108. )
  109. # Add a tag
  110. subprocess.check_call(["git", "tag", "-a", self.tags[0], "-m", "Add tag"])
  111. # Checkout a second branch
  112. subprocess.check_call(["git", "checkout", "--quiet", "-b", self.branches[1]])
  113. # Add a line to the file
  114. with salt.utils.files.fopen(self.files[0], "a") as fp_:
  115. fp_.write(salt.utils.stringutils.to_str("Added a line\n"))
  116. # Commit the updated file
  117. subprocess.check_call(
  118. [
  119. "git",
  120. "commit",
  121. "--quiet",
  122. "--message",
  123. "Added a line to " + self.files[0],
  124. self.files[0],
  125. ]
  126. )
  127. # Switch back to master
  128. subprocess.check_call(["git", "checkout", "--quiet", "master"])
  129. # Go back to original cwd
  130. os.chdir(self.orig_cwd)
  131. def run_function(self, *args, **kwargs): # pylint: disable=arguments-differ
  132. """
  133. Ensure that results are decoded
  134. TODO: maybe move this behavior to ModuleCase itself?
  135. """
  136. return salt.utils.data.decode(
  137. super(GitModuleTest, self).run_function(*args, **kwargs)
  138. )
  139. def tearDown(self):
  140. for key in ("orig_cwd", "repo", "files", "dirs", "branches", "tags"):
  141. delattr(self, key)
  142. super(GitModuleTest, self).tearDown()
  143. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  144. def test_add_dir(self):
  145. """
  146. Test git.add with a directory
  147. """
  148. newdir = "quux"
  149. # Change to the repo dir
  150. newdir_path = os.path.join(self.repo, newdir)
  151. _makedirs(newdir_path)
  152. files = [os.path.join(newdir_path, x) for x in self.files]
  153. files_relpath = [os.path.join(newdir, x) for x in self.files]
  154. for path in files:
  155. with salt.utils.files.fopen(path, "wb") as fp_:
  156. fp_.write(
  157. "This is a test file with relative path {0}.\n".format(path).encode(
  158. "utf-8"
  159. )
  160. )
  161. ret = self.run_function("git.add", [self.repo, newdir])
  162. res = "\n".join(sorted(["add '{0}'".format(x) for x in files_relpath]))
  163. if salt.utils.platform.is_windows():
  164. res = res.replace("\\", "/")
  165. self.assertEqual(ret, res)
  166. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  167. def test_add_file(self):
  168. """
  169. Test git.add with a file
  170. """
  171. filename = "quux"
  172. file_path = os.path.join(self.repo, filename)
  173. with salt.utils.files.fopen(file_path, "w") as fp_:
  174. fp_.write(
  175. salt.utils.stringutils.to_str(
  176. "This is a test file named {0}.\n".format(filename)
  177. )
  178. )
  179. ret = self.run_function("git.add", [self.repo, filename])
  180. self.assertEqual(ret, "add '{0}'".format(filename))
  181. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  182. def test_archive(self):
  183. """
  184. Test git.archive
  185. """
  186. tar_archive = os.path.join(RUNTIME_VARS.TMP, "test_archive.tar.gz")
  187. try:
  188. self.assertTrue(
  189. self.run_function(
  190. "git.archive", [self.repo, tar_archive], prefix="foo/"
  191. )
  192. )
  193. self.assertTrue(tarfile.is_tarfile(tar_archive))
  194. self.run_function("cmd.run", ["cp " + tar_archive + " /root/"])
  195. with closing(tarfile.open(tar_archive, "r")) as tar_obj:
  196. self.assertEqual(
  197. sorted(salt.utils.data.decode(tar_obj.getnames())),
  198. sorted(
  199. [
  200. "foo",
  201. "foo/bar",
  202. "foo/baz",
  203. "foo/foo",
  204. "foo/питон",
  205. "foo/qux",
  206. "foo/qux/bar",
  207. "foo/qux/baz",
  208. "foo/qux/foo",
  209. "foo/qux/питон",
  210. ]
  211. ),
  212. )
  213. finally:
  214. try:
  215. os.unlink(tar_archive)
  216. except OSError:
  217. pass
  218. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  219. def test_archive_subdir(self):
  220. """
  221. Test git.archive on a subdir, giving only a partial copy of the repo in
  222. the resulting archive
  223. """
  224. tar_archive = os.path.join(RUNTIME_VARS.TMP, "test_archive.tar.gz")
  225. try:
  226. self.assertTrue(
  227. self.run_function(
  228. "git.archive",
  229. [os.path.join(self.repo, "qux"), tar_archive],
  230. prefix="foo/",
  231. )
  232. )
  233. self.assertTrue(tarfile.is_tarfile(tar_archive))
  234. with closing(tarfile.open(tar_archive, "r")) as tar_obj:
  235. self.assertEqual(
  236. sorted(salt.utils.data.decode(tar_obj.getnames())),
  237. sorted(["foo", "foo/bar", "foo/baz", "foo/foo", "foo/питон"]),
  238. )
  239. finally:
  240. try:
  241. os.unlink(tar_archive)
  242. except OSError:
  243. pass
  244. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  245. def test_branch(self):
  246. """
  247. Test creating, renaming, and deleting a branch using git.branch
  248. """
  249. renamed_branch = "ihavebeenrenamed"
  250. self.assertTrue(self.run_function("git.branch", [self.repo, self.branches[1]]))
  251. self.assertTrue(
  252. self.run_function(
  253. "git.branch", [self.repo, renamed_branch], opts="-m " + self.branches[1]
  254. )
  255. )
  256. self.assertTrue(
  257. self.run_function("git.branch", [self.repo, renamed_branch], opts="-D")
  258. )
  259. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  260. def test_checkout(self):
  261. """
  262. Test checking out a new branch and then checking out master again
  263. """
  264. new_branch = "iamanothernewbranch"
  265. self.assertEqual(
  266. self.run_function(
  267. "git.checkout", [self.repo, "HEAD"], opts="-b " + new_branch
  268. ),
  269. "Switched to a new branch '" + new_branch + "'",
  270. )
  271. self.assertTrue(
  272. "Switched to branch 'master'"
  273. in self.run_function("git.checkout", [self.repo, "master"]),
  274. )
  275. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  276. def test_checkout_no_rev(self):
  277. """
  278. Test git.checkout without a rev, both with -b in opts and without
  279. """
  280. new_branch = "iamanothernewbranch"
  281. self.assertEqual(
  282. self.run_function(
  283. "git.checkout", [self.repo], rev=None, opts="-b " + new_branch
  284. ),
  285. "Switched to a new branch '" + new_branch + "'",
  286. )
  287. self.assertTrue(
  288. "'rev' argument is required unless -b or -B in opts"
  289. in self.run_function("git.checkout", [self.repo])
  290. )
  291. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  292. def test_clone(self):
  293. """
  294. Test cloning an existing repo
  295. """
  296. clone_parent_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  297. self.assertTrue(self.run_function("git.clone", [clone_parent_dir, self.repo]))
  298. # Cleanup after yourself
  299. shutil.rmtree(clone_parent_dir, True)
  300. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  301. def test_clone_with_alternate_name(self):
  302. """
  303. Test cloning an existing repo with an alternate name for the repo dir
  304. """
  305. clone_parent_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  306. clone_name = os.path.basename(self.repo)
  307. # Change to newly-created temp dir
  308. self.assertTrue(
  309. self.run_function(
  310. "git.clone", [clone_parent_dir, self.repo], name=clone_name
  311. )
  312. )
  313. # Cleanup after yourself
  314. shutil.rmtree(clone_parent_dir, True)
  315. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  316. def test_commit(self):
  317. """
  318. Test git.commit two ways:
  319. 1) First using git.add, then git.commit
  320. 2) Using git.commit with the 'filename' argument to skip staging
  321. """
  322. filename = "foo"
  323. commit_re_prefix = r"^\[master [0-9a-f]+\] "
  324. # Add a line
  325. with salt.utils.files.fopen(os.path.join(self.repo, filename), "a") as fp_:
  326. fp_.write("Added a line\n")
  327. # Stage the file
  328. self.run_function("git.add", [self.repo, filename])
  329. # Commit the staged file
  330. commit_msg = "Add a line to " + filename
  331. ret = self.run_function("git.commit", [self.repo, commit_msg])
  332. # Make sure the expected line is in the output
  333. self.assertTrue(bool(re.search(commit_re_prefix + commit_msg, ret)))
  334. # Add another line
  335. with salt.utils.files.fopen(os.path.join(self.repo, filename), "a") as fp_:
  336. fp_.write("Added another line\n")
  337. # Commit the second file without staging
  338. commit_msg = "Add another line to " + filename
  339. ret = self.run_function(
  340. "git.commit", [self.repo, commit_msg], filename=filename
  341. )
  342. self.assertTrue(bool(re.search(commit_re_prefix + commit_msg, ret)))
  343. @pytest.mark.slow_test(seconds=240) # Test takes >120 and <=240 seconds
  344. def test_config(self):
  345. """
  346. Test setting, getting, and unsetting config values
  347. WARNING: This test will modify and completely remove a config section
  348. 'foo', both in the repo created in setUp() and in the user's global
  349. .gitconfig.
  350. """
  351. def _clear_config():
  352. cmds = (
  353. ["git", "config", "--remove-section", "foo"],
  354. ["git", "config", "--global", "--remove-section", "foo"],
  355. )
  356. for cmd in cmds:
  357. with salt.utils.files.fopen(os.devnull, "w") as devnull:
  358. try:
  359. subprocess.check_call(cmd, stderr=devnull)
  360. except subprocess.CalledProcessError:
  361. pass
  362. cfg_local = {"foo.single": ["foo"], "foo.multi": ["foo", "bar", "baz"]}
  363. cfg_global = {"foo.single": ["abc"], "foo.multi": ["abc", "def", "ghi"]}
  364. _clear_config()
  365. try:
  366. log.debug("Try to specify both single and multivar (should raise error)")
  367. self.assertTrue(
  368. "Only one of 'value' and 'multivar' is permitted"
  369. in self.run_function(
  370. "git.config_set",
  371. ["foo.single"],
  372. value=cfg_local["foo.single"][0],
  373. multivar=cfg_local["foo.multi"],
  374. cwd=self.repo,
  375. )
  376. )
  377. log.debug(
  378. "Try to set single local value without cwd (should raise " "error)"
  379. )
  380. self.assertTrue(
  381. "'cwd' argument required unless global=True"
  382. in self.run_function(
  383. "git.config_set", ["foo.single"], value=cfg_local["foo.single"][0],
  384. )
  385. )
  386. log.debug("Set single local value")
  387. self.assertEqual(
  388. self.run_function(
  389. "git.config_set",
  390. ["foo.single"],
  391. value=cfg_local["foo.single"][0],
  392. cwd=self.repo,
  393. ),
  394. cfg_local["foo.single"],
  395. )
  396. log.debug("Set single global value")
  397. self.assertEqual(
  398. self.run_function(
  399. "git.config_set",
  400. ["foo.single"],
  401. value=cfg_global["foo.single"][0],
  402. **{"global": True}
  403. ),
  404. cfg_global["foo.single"],
  405. )
  406. log.debug("Set local multivar")
  407. self.assertEqual(
  408. self.run_function(
  409. "git.config_set",
  410. ["foo.multi"],
  411. multivar=cfg_local["foo.multi"],
  412. cwd=self.repo,
  413. ),
  414. cfg_local["foo.multi"],
  415. )
  416. log.debug("Set global multivar")
  417. self.assertEqual(
  418. self.run_function(
  419. "git.config_set",
  420. ["foo.multi"],
  421. multivar=cfg_global["foo.multi"],
  422. **{"global": True}
  423. ),
  424. cfg_global["foo.multi"],
  425. )
  426. log.debug("Get single local value")
  427. self.assertEqual(
  428. self.run_function("git.config_get", ["foo.single"], cwd=self.repo),
  429. cfg_local["foo.single"][0],
  430. )
  431. log.debug("Get single value from local multivar")
  432. self.assertEqual(
  433. self.run_function("git.config_get", ["foo.multi"], cwd=self.repo),
  434. cfg_local["foo.multi"][-1],
  435. )
  436. log.debug("Get all values from multivar (includes globals)")
  437. self.assertEqual(
  438. self.run_function(
  439. "git.config_get", ["foo.multi"], cwd=self.repo, **{"all": True}
  440. ),
  441. cfg_local["foo.multi"],
  442. )
  443. log.debug("Get single global value")
  444. self.assertEqual(
  445. self.run_function("git.config_get", ["foo.single"], **{"global": True}),
  446. cfg_global["foo.single"][0],
  447. )
  448. log.debug("Get single value from global multivar")
  449. self.assertEqual(
  450. self.run_function("git.config_get", ["foo.multi"], **{"global": True}),
  451. cfg_global["foo.multi"][-1],
  452. )
  453. log.debug("Get all values from global multivar")
  454. self.assertEqual(
  455. self.run_function(
  456. "git.config_get", ["foo.multi"], **{"all": True, "global": True}
  457. ),
  458. cfg_global["foo.multi"],
  459. )
  460. log.debug("Get all local keys/values using regex")
  461. self.assertEqual(
  462. self.run_function(
  463. "git.config_get_regexp", ["foo.(single|multi)"], cwd=self.repo
  464. ),
  465. cfg_local,
  466. )
  467. log.debug("Get all global keys/values using regex")
  468. self.assertEqual(
  469. self.run_function(
  470. "git.config_get_regexp",
  471. ["foo.(single|multi)"],
  472. cwd=self.repo,
  473. **{"global": True}
  474. ),
  475. cfg_global,
  476. )
  477. log.debug("Get just the local foo.multi values containing 'a'")
  478. self.assertEqual(
  479. self.run_function(
  480. "git.config_get_regexp",
  481. ["foo.multi"],
  482. value_regex="a",
  483. cwd=self.repo,
  484. ),
  485. {"foo.multi": [x for x in cfg_local["foo.multi"] if "a" in x]},
  486. )
  487. log.debug("Get just the global foo.multi values containing 'a'")
  488. self.assertEqual(
  489. self.run_function(
  490. "git.config_get_regexp",
  491. ["foo.multi"],
  492. value_regex="a",
  493. cwd=self.repo,
  494. **{"global": True}
  495. ),
  496. {"foo.multi": [x for x in cfg_global["foo.multi"] if "a" in x]},
  497. )
  498. # TODO: More robust unset testing, try to trigger all the
  499. # exceptions raised.
  500. log.debug("Unset a single local value")
  501. self.assertTrue(
  502. self.run_function("git.config_unset", ["foo.single"], cwd=self.repo,)
  503. )
  504. log.debug("Unset an entire local multivar")
  505. self.assertTrue(
  506. self.run_function(
  507. "git.config_unset", ["foo.multi"], cwd=self.repo, **{"all": True}
  508. )
  509. )
  510. log.debug("Unset a single global value")
  511. self.assertTrue(
  512. self.run_function(
  513. "git.config_unset", ["foo.single"], **{"global": True}
  514. )
  515. )
  516. log.debug("Unset an entire local multivar")
  517. self.assertTrue(
  518. self.run_function(
  519. "git.config_unset", ["foo.multi"], **{"all": True, "global": True}
  520. )
  521. )
  522. finally:
  523. _clear_config()
  524. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  525. def test_current_branch(self):
  526. """
  527. Test git.current_branch
  528. """
  529. self.assertEqual(self.run_function("git.current_branch", [self.repo]), "master")
  530. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  531. def test_describe(self):
  532. """
  533. Test git.describe
  534. """
  535. self.assertEqual(self.run_function("git.describe", [self.repo]), self.tags[0])
  536. # Test for git.fetch would be unreliable on Jenkins, skipping for now
  537. # The test should go into test_remotes when ready
  538. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  539. def test_init(self):
  540. """
  541. Use git.init to init a new repo
  542. """
  543. new_repo = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  544. # `tempfile.mkdtemp` gets the path to the Temp directory using
  545. # environment variables. As a result, folder names longer than 8
  546. # characters are shortened. For example "C:\Users\Administrators"
  547. # becomes "C:\Users\Admini~1". However, the "git.init" function returns
  548. # the full, unshortened name of the folder. Therefore you can't compare
  549. # the path returned by `tempfile.mkdtemp` and the results of `git.init`
  550. # exactly.
  551. if salt.utils.platform.is_windows():
  552. new_repo = new_repo.replace("\\", "/")
  553. # Get the name of the temp directory
  554. tmp_dir = os.path.basename(new_repo)
  555. # Get git output
  556. git_ret = self.run_function("git.init", [new_repo]).lower()
  557. self.assertIn("Initialized empty Git repository in".lower(), git_ret)
  558. self.assertIn(tmp_dir, git_ret)
  559. else:
  560. self.assertEqual(
  561. self.run_function("git.init", [new_repo]).lower(),
  562. "Initialized empty Git repository in {0}/.git/".format(
  563. new_repo
  564. ).lower(),
  565. )
  566. shutil.rmtree(new_repo)
  567. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  568. def test_list_branches(self):
  569. """
  570. Test git.list_branches
  571. """
  572. self.assertEqual(
  573. self.run_function("git.list_branches", [self.repo]), sorted(self.branches)
  574. )
  575. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  576. def test_list_tags(self):
  577. """
  578. Test git.list_tags
  579. """
  580. self.assertEqual(
  581. self.run_function("git.list_tags", [self.repo]), sorted(self.tags)
  582. )
  583. # Test for git.ls_remote will need to wait for now, while I think of how to
  584. # properly mock it.
  585. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  586. def test_merge(self):
  587. """
  588. Test git.merge
  589. # TODO: Test more than just a fast-forward merge
  590. """
  591. # Merge the second branch into the current branch
  592. ret = self.run_function("git.merge", [self.repo], rev=self.branches[1])
  593. # Merge should be a fast-forward
  594. self.assertTrue("Fast-forward" in ret.splitlines())
  595. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  596. def test_merge_base_and_tree(self):
  597. """
  598. Test git.merge_base, git.merge_tree and git.revision
  599. TODO: Test all of the arguments
  600. """
  601. # Get the SHA1 of current HEAD
  602. head_rev = self.run_function("git.revision", [self.repo], rev="HEAD")
  603. # Make sure revision is a 40-char string
  604. self.assertTrue(len(head_rev) == 40)
  605. # Get the second branch's SHA1
  606. second_rev = self.run_function(
  607. "git.revision", [self.repo], rev=self.branches[1], timeout=120
  608. )
  609. # Make sure revision is a 40-char string
  610. self.assertTrue(len(second_rev) == 40)
  611. # self.branches[1] should be just one commit ahead, so the merge base
  612. # for master and self.branches[1] should be the same as the current
  613. # HEAD.
  614. self.assertEqual(
  615. self.run_function(
  616. "git.merge_base", [self.repo], refs=",".join((head_rev, second_rev))
  617. ),
  618. head_rev,
  619. )
  620. # There should be no conflict here, so the return should be an empty
  621. # string.
  622. ret = self.run_function(
  623. "git.merge_tree", [self.repo, head_rev, second_rev]
  624. ).splitlines()
  625. self.assertTrue(len([x for x in ret if x.startswith("@@")]) == 1)
  626. # Test for git.pull would be unreliable on Jenkins, skipping for now
  627. # Test for git.push would be unreliable on Jenkins, skipping for now
  628. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  629. def test_rebase(self):
  630. """
  631. Test git.rebase
  632. """
  633. # Make a change to a different file than the one modifed in setUp
  634. file_path = os.path.join(self.repo, self.files[1])
  635. with salt.utils.files.fopen(file_path, "a") as fp_:
  636. fp_.write("Added a line\n")
  637. # Commit the change
  638. self.assertTrue(
  639. "ERROR"
  640. not in self.run_function(
  641. "git.commit",
  642. [self.repo, "Added a line to " + self.files[1]],
  643. filename=self.files[1],
  644. )
  645. )
  646. # Switch to the second branch
  647. self.assertTrue(
  648. "ERROR"
  649. not in self.run_function("git.checkout", [self.repo], rev=self.branches[1])
  650. )
  651. # Perform the rebase. The commit should show a comment about
  652. # self.files[0] being modified, as that is the file that was modified
  653. # in the second branch in the setUp function
  654. self.assertEqual(
  655. self.run_function("git.rebase", [self.repo]),
  656. "First, rewinding head to replay your work on top of it...\n"
  657. "Applying: Added a line to " + self.files[0],
  658. )
  659. # Test for git.remote_get is in test_remotes
  660. # Test for git.remote_set is in test_remotes
  661. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  662. def test_remotes(self):
  663. """
  664. Test setting a remote (git.remote_set), and getting a remote
  665. (git.remote_get and git.remotes)
  666. TODO: Properly mock fetching a remote (git.fetch), and build out more
  667. robust testing that confirms that the https auth bits work.
  668. """
  669. remotes = {
  670. "first": {"fetch": "/dev/null", "push": "/dev/null"},
  671. "second": {"fetch": "/dev/null", "push": "/dev/stdout"},
  672. }
  673. self.assertEqual(
  674. self.run_function(
  675. "git.remote_set", [self.repo, remotes["first"]["fetch"]], remote="first"
  676. ),
  677. remotes["first"],
  678. )
  679. self.assertEqual(
  680. self.run_function(
  681. "git.remote_set",
  682. [self.repo, remotes["second"]["fetch"]],
  683. remote="second",
  684. push_url=remotes["second"]["push"],
  685. ),
  686. remotes["second"],
  687. )
  688. self.assertEqual(self.run_function("git.remotes", [self.repo]), remotes)
  689. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  690. def test_reset(self):
  691. """
  692. Test git.reset
  693. TODO: Test more than just a hard reset
  694. """
  695. # Switch to the second branch
  696. self.assertTrue(
  697. "ERROR"
  698. not in self.run_function("git.checkout", [self.repo], rev=self.branches[1])
  699. )
  700. # Back up one commit. We should now be at the same revision as master
  701. self.run_function("git.reset", [self.repo], opts="--hard HEAD~1")
  702. # Get the SHA1 of current HEAD (remember, we're on the second branch)
  703. head_rev = self.run_function("git.revision", [self.repo], rev="HEAD")
  704. # Make sure revision is a 40-char string
  705. self.assertTrue(len(head_rev) == 40)
  706. # Get the master branch's SHA1
  707. master_rev = self.run_function("git.revision", [self.repo], rev="master")
  708. # Make sure revision is a 40-char string
  709. self.assertTrue(len(master_rev) == 40)
  710. # The two revisions should be the same
  711. self.assertEqual(head_rev, master_rev)
  712. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  713. def test_rev_parse(self):
  714. """
  715. Test git.rev_parse
  716. """
  717. # Using --abbrev-ref on HEAD will give us the current branch
  718. self.assertEqual(
  719. self.run_function(
  720. "git.rev_parse", [self.repo, "HEAD"], opts="--abbrev-ref"
  721. ),
  722. "master",
  723. )
  724. # Test for git.revision happens in test_merge_base
  725. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  726. def test_rm(self):
  727. """
  728. Test git.rm
  729. """
  730. single_file = self.files[0]
  731. entire_dir = self.dirs[1]
  732. # Remove a single file
  733. self.assertEqual(
  734. self.run_function("git.rm", [self.repo, single_file]),
  735. "rm '" + single_file + "'",
  736. )
  737. # Remove an entire dir
  738. expected = "\n".join(
  739. sorted(["rm '" + os.path.join(entire_dir, x) + "'" for x in self.files])
  740. )
  741. if salt.utils.platform.is_windows():
  742. expected = expected.replace("\\", "/")
  743. self.assertEqual(
  744. self.run_function("git.rm", [self.repo, entire_dir], opts="-r"), expected
  745. )
  746. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  747. def test_stash(self):
  748. """
  749. Test git.stash
  750. # TODO: test more stash actions
  751. """
  752. file_path = os.path.join(self.repo, self.files[0])
  753. with salt.utils.files.fopen(file_path, "a") as fp_:
  754. fp_.write("Temp change to be stashed")
  755. self.assertTrue("ERROR" not in self.run_function("git.stash", [self.repo]))
  756. # List stashes
  757. ret = self.run_function("git.stash", [self.repo], action="list")
  758. self.assertTrue("ERROR" not in ret)
  759. self.assertTrue(len(ret.splitlines()) == 1)
  760. # Apply the stash
  761. self.assertTrue(
  762. "ERROR"
  763. not in self.run_function(
  764. "git.stash", [self.repo], action="apply", opts="stash@{0}"
  765. )
  766. )
  767. # Drop the stash
  768. self.assertTrue(
  769. "ERROR"
  770. not in self.run_function(
  771. "git.stash", [self.repo], action="drop", opts="stash@{0}"
  772. )
  773. )
  774. @pytest.mark.slow_test(seconds=60) # Test takes >30 and <=60 seconds
  775. def test_status(self):
  776. """
  777. Test git.status
  778. """
  779. changes = {
  780. "modified": ["foo"],
  781. "new": ["thisisdefinitelyanewfile"],
  782. "deleted": ["bar"],
  783. "untracked": ["thisisalsoanewfile"],
  784. }
  785. for filename in changes["modified"]:
  786. with salt.utils.files.fopen(os.path.join(self.repo, filename), "a") as fp_:
  787. fp_.write("Added a line\n")
  788. for filename in changes["new"]:
  789. with salt.utils.files.fopen(os.path.join(self.repo, filename), "w") as fp_:
  790. fp_.write(
  791. salt.utils.stringutils.to_str(
  792. "This is a new file named {0}.".format(filename)
  793. )
  794. )
  795. # Stage the new file so it shows up as a 'new' file
  796. self.assertTrue(
  797. "ERROR" not in self.run_function("git.add", [self.repo, filename])
  798. )
  799. for filename in changes["deleted"]:
  800. self.run_function("git.rm", [self.repo, filename])
  801. for filename in changes["untracked"]:
  802. with salt.utils.files.fopen(os.path.join(self.repo, filename), "w") as fp_:
  803. fp_.write(
  804. salt.utils.stringutils.to_str(
  805. "This is a new file named {0}.".format(filename)
  806. )
  807. )
  808. self.assertEqual(self.run_function("git.status", [self.repo]), changes)
  809. # TODO: Add git.submodule test
  810. @pytest.mark.slow_test(seconds=30) # Test takes >10 and <=30 seconds
  811. def test_symbolic_ref(self):
  812. """
  813. Test git.symbolic_ref
  814. """
  815. self.assertEqual(
  816. self.run_function("git.symbolic_ref", [self.repo, "HEAD"], opts="--quiet"),
  817. "refs/heads/master",
  818. )
  819. @skipIf(
  820. not _worktrees_supported(), "Git 2.5 or newer required for worktree support"
  821. )
  822. @pytest.mark.slow_test(seconds=120) # Test takes >60 and <=120 seconds
  823. def test_worktree_add_rm(self):
  824. """
  825. This tests git.worktree_add, git.is_worktree, git.worktree_rm, and
  826. git.worktree_prune. Tests for 'git worktree list' are covered in
  827. tests.unit.modules.git_test.
  828. """
  829. # We don't need to enclose this comparison in a try/except, since the
  830. # decorator would skip this test if git is not installed and we'd never
  831. # get here in the first place.
  832. if _git_version() >= LooseVersion("2.6.0"):
  833. worktree_add_prefix = "Preparing "
  834. else:
  835. worktree_add_prefix = "Enter "
  836. worktree_path = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  837. worktree_basename = os.path.basename(worktree_path)
  838. worktree_path2 = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  839. worktree_basename2 = os.path.basename(worktree_path2)
  840. # Even though this is Windows, git commands return a unix style path
  841. if salt.utils.platform.is_windows():
  842. worktree_path = worktree_path.replace("\\", "/")
  843. worktree_path2 = worktree_path2.replace("\\", "/")
  844. # Add the worktrees
  845. ret = self.run_function("git.worktree_add", [self.repo, worktree_path],)
  846. self.assertTrue(worktree_add_prefix in ret)
  847. self.assertTrue(worktree_basename in ret)
  848. ret = self.run_function("git.worktree_add", [self.repo, worktree_path2])
  849. self.assertTrue(worktree_add_prefix in ret)
  850. self.assertTrue(worktree_basename2 in ret)
  851. # Check if this new path is a worktree
  852. self.assertTrue(self.run_function("git.is_worktree", [worktree_path]))
  853. # Check if the main repo is a worktree
  854. self.assertFalse(self.run_function("git.is_worktree", [self.repo]))
  855. # Check if a non-repo directory is a worktree
  856. empty_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  857. self.assertFalse(self.run_function("git.is_worktree", [empty_dir]))
  858. shutil.rmtree(empty_dir)
  859. # Remove the first worktree
  860. self.assertTrue(self.run_function("git.worktree_rm", [worktree_path]))
  861. # Prune the worktrees
  862. prune_message = (
  863. "Removing worktrees/{0}: gitdir file points to non-existent "
  864. "location".format(worktree_basename)
  865. )
  866. # Test dry run output. It should match the same output we get when we
  867. # actually prune the worktrees.
  868. result = self.run_function("git.worktree_prune", [self.repo], dry_run=True)
  869. self.assertEqual(result, prune_message)
  870. # Test pruning for real, and make sure the output is the same
  871. self.assertEqual(
  872. self.run_function("git.worktree_prune", [self.repo]), prune_message
  873. )