1
0

test_git.py 35 KB

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