hooks_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2023 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. import os
  6. import os.path
  7. import sys
  8. import tempfile
  9. import unittest
  10. import unittest.mock
  11. from unittest.mock import patch
  12. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  13. sys.path.insert(0, ROOT_DIR)
  14. from gclient import PRECOMMIT_HOOK_VAR
  15. import gclient_utils
  16. from gclient_eval import SYNC, SUBMODULES
  17. import git_common as git
  18. class HooksTest(unittest.TestCase):
  19. def setUp(self):
  20. super(HooksTest, self).setUp()
  21. self.repo = tempfile.mkdtemp()
  22. self.env = os.environ.copy()
  23. self.env['SKIP_GITLINK_PRECOMMIT'] = '0'
  24. self.env['TESTING_ANSWER'] = 'n'
  25. self.populate()
  26. def tearDown(self):
  27. gclient_utils.rmtree(self.repo)
  28. def write(self, repo, path, content):
  29. with open(os.path.join(repo, path), 'w') as f:
  30. f.write(content)
  31. def populate(self):
  32. git.run('init', cwd=self.repo)
  33. deps_content = '\n'.join((
  34. f'git_dependencies = "{SYNC}"',
  35. 'deps = {',
  36. f' "dep_a": "host://dep_a@{"a"*40}",',
  37. f' "dep_b": "host://dep_b@{"b"*40}",',
  38. '}',
  39. ))
  40. self.write(self.repo, 'DEPS', deps_content)
  41. self.dep_a_repo = os.path.join(self.repo, 'dep_a')
  42. os.mkdir(self.dep_a_repo)
  43. git.run('init', cwd=self.dep_a_repo)
  44. os.mkdir(os.path.join(self.repo, 'dep_b'))
  45. gitmodules_content = '\n'.join((
  46. '[submodule "dep_a"]'
  47. '\tpath = dep_a',
  48. '\turl = host://dep_a',
  49. '[submodule "dep_b"]'
  50. '\tpath = dep_b',
  51. '\turl = host://dep_b',
  52. ))
  53. self.write(self.repo, '.gitmodules', gitmodules_content)
  54. git.run('update-index',
  55. '--add',
  56. '--cacheinfo',
  57. f'160000,{"a"*40},dep_a',
  58. cwd=self.repo)
  59. git.run('update-index',
  60. '--add',
  61. '--cacheinfo',
  62. f'160000,{"b"*40},dep_b',
  63. cwd=self.repo)
  64. git.run('add', '.', cwd=self.repo)
  65. git.run('commit', '-m', 'init', cwd=self.repo)
  66. # On Windows, this path is written to the file as
  67. # "root_dir\hooks\pre-commit.py", but it gets interpreted as
  68. # "root_dirhookspre-commit.py".
  69. precommit_path = os.path.join(ROOT_DIR, 'hooks',
  70. 'pre-commit.py').replace('\\', '\\\\')
  71. precommit_content = '\n'.join((
  72. '#!/bin/sh',
  73. f'{PRECOMMIT_HOOK_VAR}={precommit_path}',
  74. f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
  75. f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
  76. 'fi',
  77. ))
  78. self.write(self.repo, os.path.join('.git', 'hooks', 'pre-commit'),
  79. precommit_content)
  80. os.chmod(os.path.join(self.repo, '.git', 'hooks', 'pre-commit'), 0o755)
  81. def testPreCommit_NoGitlinkOrDEPS(self):
  82. # Sanity check. Neither gitlinks nor DEPS are touched.
  83. self.write(self.repo, 'foo', 'foo')
  84. git.run('add', '.', cwd=self.repo)
  85. expected_diff = git.run('diff', '--cached', cwd=self.repo)
  86. git.run('commit', '-m', 'foo', cwd=self.repo)
  87. self.assertEqual(expected_diff,
  88. git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo))
  89. def testPreCommit_GitlinkWithoutDEPS(self):
  90. # Gitlink changes were staged without a corresponding DEPS change.
  91. self.write(self.repo, 'foo', 'foo')
  92. git.run('add', '.', cwd=self.repo)
  93. git.run('update-index',
  94. '--replace',
  95. '--cacheinfo',
  96. f'160000,{"b"*40},dep_a',
  97. cwd=self.repo)
  98. git.run('update-index',
  99. '--replace',
  100. '--cacheinfo',
  101. f'160000,{"a"*40},dep_b',
  102. cwd=self.repo)
  103. diff_before_commit = git.run('diff',
  104. '--cached',
  105. '--name-only',
  106. cwd=self.repo)
  107. _, stderr = git.run_with_stderr('commit',
  108. '-m',
  109. 'regular file and gitlinks',
  110. cwd=self.repo,
  111. env=self.env)
  112. self.assertIn('dep_a', diff_before_commit)
  113. self.assertIn('dep_b', diff_before_commit)
  114. # Gitlinks should be dropped.
  115. self.assertIn(
  116. 'Found no change to DEPS, but found staged gitlink(s) in diff',
  117. stderr)
  118. diff_after_commit = git.run('diff',
  119. '--name-only',
  120. 'HEAD^',
  121. 'HEAD',
  122. cwd=self.repo)
  123. self.assertNotIn('dep_a', diff_after_commit)
  124. self.assertNotIn('dep_b', diff_after_commit)
  125. self.assertIn('foo', diff_after_commit)
  126. def testPreCommit_IntentionalGitlinkWithoutDEPS(self):
  127. # Intentional Gitlink changes staged without a DEPS change.
  128. self.write(self.repo, 'foo', 'foo')
  129. git.run('add', '.', cwd=self.repo)
  130. git.run('update-index',
  131. '--replace',
  132. '--cacheinfo',
  133. f'160000,{"b"*40},dep_a',
  134. cwd=self.repo)
  135. git.run('update-index',
  136. '--replace',
  137. '--cacheinfo',
  138. f'160000,{"a"*40},dep_b',
  139. cwd=self.repo)
  140. diff_before_commit = git.run('diff',
  141. '--cached',
  142. '--name-only',
  143. cwd=self.repo)
  144. self.env['TESTING_ANSWER'] = ''
  145. _, stderr = git.run_with_stderr('commit',
  146. '-m',
  147. 'regular file and gitlinks',
  148. cwd=self.repo,
  149. env=self.env)
  150. self.assertIn('dep_a', diff_before_commit)
  151. self.assertIn('dep_b', diff_before_commit)
  152. # Gitlinks should be dropped.
  153. self.assertIn(
  154. 'Found no change to DEPS, but found staged gitlink(s) in diff',
  155. stderr)
  156. diff_after_commit = git.run('diff',
  157. '--name-only',
  158. 'HEAD^',
  159. 'HEAD',
  160. cwd=self.repo)
  161. self.assertIn('dep_a', diff_after_commit)
  162. self.assertIn('dep_b', diff_after_commit)
  163. self.assertIn('foo', diff_after_commit)
  164. def testPreCommit_OnlyGitlinkWithoutDEPS(self):
  165. # Gitlink changes were staged without a corresponding DEPS change but
  166. # no other files were included in the commit.
  167. git.run('update-index',
  168. '--replace',
  169. '--cacheinfo',
  170. f'160000,{"b"*40},dep_a',
  171. cwd=self.repo)
  172. diff_before_commit = git.run('diff',
  173. '--cached',
  174. '--name-only',
  175. cwd=self.repo)
  176. ret = git.run_with_retcode('commit',
  177. '-m',
  178. 'gitlink only',
  179. cwd=self.repo,
  180. env=self.env)
  181. self.assertIn('dep_a', diff_before_commit)
  182. # Gitlinks should be droppped and the empty commit should be aborted.
  183. self.assertEqual(ret, 1)
  184. diff_after_commit = git.run('diff',
  185. '--cached',
  186. '--name-only',
  187. cwd=self.repo)
  188. self.assertNotIn('dep_a', diff_after_commit)
  189. def testPreCommit_CommitAll(self):
  190. self.write(self.repo, 'foo', 'foo')
  191. git.run('add', '.', cwd=self.repo)
  192. git.run('commit', '-m', 'add foo', cwd=self.repo)
  193. self.write(self.repo, 'foo', 'foo2')
  194. # Create a new commit in dep_a.
  195. self.write(self.dep_a_repo, 'sub_foo', 'sub_foo')
  196. git.run('add', '.', cwd=self.dep_a_repo)
  197. git.run('commit', '-m', 'sub_foo', cwd=self.dep_a_repo)
  198. diff_before_commit = git.run('status',
  199. cwd=self.repo)
  200. self.assertIn('foo', diff_before_commit)
  201. self.assertIn('dep_a', diff_before_commit)
  202. ret = git.run_with_retcode('commit',
  203. '--all',
  204. '-m',
  205. 'commit all',
  206. cwd=self.repo,
  207. env=self.env)
  208. self.assertIn('dep_a', diff_before_commit)
  209. self.assertEqual(ret, 0)
  210. diff_after_commit = git.run('diff',
  211. '--cached',
  212. '--name-only',
  213. cwd=self.repo)
  214. self.assertNotIn('dep_a', diff_after_commit)
  215. diff_from_commit = git.run('diff',
  216. '--name-only',
  217. 'HEAD^',
  218. 'HEAD',
  219. cwd=self.repo)
  220. self.assertIn('foo', diff_from_commit)
  221. def testPreCommit_GitlinkWithDEPS(self):
  222. # A gitlink was staged with a corresponding DEPS change.
  223. updated_deps = '\n'.join((
  224. f'git_dependencies = "{SYNC}"',
  225. 'deps = {',
  226. f' "dep_a": "host://dep_a@{"b"*40}",',
  227. f' "dep_b": "host://dep_b@{"b"*40}",',
  228. '}',
  229. ))
  230. self.write(self.repo, 'DEPS', updated_deps)
  231. git.run('add', '.', cwd=self.repo)
  232. git.run('update-index',
  233. '--replace',
  234. '--cacheinfo',
  235. f'160000,{"b"*40},dep_a',
  236. cwd=self.repo)
  237. diff_before_commit = git.run('diff', '--cached', cwd=self.repo)
  238. git.run('commit', '-m', 'gitlink and DEPS', cwd=self.repo)
  239. # There should be no changes to the commit.
  240. diff_after_commit = git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo)
  241. self.assertEqual(diff_before_commit, diff_after_commit)
  242. def testPreCommit_SkipPrecommit(self):
  243. # A gitlink was staged without a corresponding DEPS change but the
  244. # SKIP_GITLINK_PRECOMMIT envvar was set.
  245. git.run('update-index',
  246. '--replace',
  247. '--cacheinfo',
  248. f'160000,{"b"*40},dep_a',
  249. cwd=self.repo)
  250. diff_before_commit = git.run('diff',
  251. '--cached',
  252. '--name-only',
  253. cwd=self.repo)
  254. self.env['SKIP_GITLINK_PRECOMMIT'] = '1'
  255. git.run('commit',
  256. '-m',
  257. 'gitlink only, skipping precommit',
  258. cwd=self.repo,
  259. env=self.env)
  260. # Gitlink should be kept.
  261. self.assertIn('dep_a', diff_before_commit)
  262. diff_after_commit = git.run('diff',
  263. '--name-only',
  264. 'HEAD^',
  265. 'HEAD',
  266. cwd=self.repo)
  267. self.assertIn('dep_a', diff_after_commit)
  268. def testPreCommit_OtherDEPSState(self):
  269. # DEPS is set to a git_dependencies state other than SYNC.
  270. deps_content = '\n'.join((
  271. f'git_dependencies = \'{SUBMODULES}\'',
  272. 'deps = {',
  273. f' "dep_a": "host://dep_a@{"a"*40}",',
  274. f' "dep_b": "host://dep_b@{"b"*40}",',
  275. '}',
  276. ))
  277. self.write(self.repo, 'DEPS', deps_content)
  278. git.run('add', '.', cwd=self.repo)
  279. git.run('commit', '-m', 'change git_dependencies', cwd=self.repo)
  280. git.run('update-index',
  281. '--replace',
  282. '--cacheinfo',
  283. f'160000,{"b"*40},dep_a',
  284. cwd=self.repo)
  285. diff_before_commit = git.run('diff', '--cached', cwd=self.repo)
  286. git.run('commit', '-m', 'update dep_a', cwd=self.repo)
  287. # There should be no changes to the commit.
  288. diff_after_commit = git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo)
  289. self.assertEqual(diff_before_commit, diff_after_commit)
  290. if __name__ == '__main__':
  291. unittest.main()