test_git.py 33 KB

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