test_git.py 34 KB

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