test_git.py 35 KB

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