#!/usr/bin/env python3 # Copyright (c) 2023 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import os import os.path import sys import tempfile import unittest import unittest.mock from unittest.mock import patch ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT_DIR) from gclient import PRECOMMIT_HOOK_VAR import gclient_utils from gclient_eval import SYNC, SUBMODULES import git_common as git class HooksTest(unittest.TestCase): def setUp(self): super(HooksTest, self).setUp() self.repo = tempfile.mkdtemp() self.env = os.environ.copy() self.env['SKIP_GITLINK_PRECOMMIT'] = '0' self.env['TESTING_ANSWER'] = 'n' self.populate() def tearDown(self): gclient_utils.rmtree(self.repo) def write(self, repo, path, content): with open(os.path.join(repo, path), 'w') as f: f.write(content) def populate(self): git.run('init', cwd=self.repo) deps_content = '\n'.join(( f'git_dependencies = "{SYNC}"', 'deps = {', f' "dep_a": "host://dep_a@{"a"*40}",', f' "dep_b": "host://dep_b@{"b"*40}",', '}', )) self.write(self.repo, 'DEPS', deps_content) self.dep_a_repo = os.path.join(self.repo, 'dep_a') os.mkdir(self.dep_a_repo) git.run('init', cwd=self.dep_a_repo) os.mkdir(os.path.join(self.repo, 'dep_b')) gitmodules_content = '\n'.join(( '[submodule "dep_a"]' '\tpath = dep_a', '\turl = host://dep_a', '[submodule "dep_b"]' '\tpath = dep_b', '\turl = host://dep_b', )) self.write(self.repo, '.gitmodules', gitmodules_content) git.run('update-index', '--add', '--cacheinfo', f'160000,{"a"*40},dep_a', cwd=self.repo) git.run('update-index', '--add', '--cacheinfo', f'160000,{"b"*40},dep_b', cwd=self.repo) git.run('add', '.', cwd=self.repo) git.run('commit', '-m', 'init', cwd=self.repo) # On Windows, this path is written to the file as # "root_dir\hooks\pre-commit.py", but it gets interpreted as # "root_dirhookspre-commit.py". precommit_path = os.path.join(ROOT_DIR, 'hooks', 'pre-commit.py').replace('\\', '\\\\') precommit_content = '\n'.join(( '#!/bin/sh', f'{PRECOMMIT_HOOK_VAR}={precommit_path}', f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then', f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1', 'fi', )) self.write(self.repo, os.path.join('.git', 'hooks', 'pre-commit'), precommit_content) os.chmod(os.path.join(self.repo, '.git', 'hooks', 'pre-commit'), 0o755) def testPreCommit_NoGitlinkOrDEPS(self): # Sanity check. Neither gitlinks nor DEPS are touched. self.write(self.repo, 'foo', 'foo') git.run('add', '.', cwd=self.repo) expected_diff = git.run('diff', '--cached', cwd=self.repo) git.run('commit', '-m', 'foo', cwd=self.repo) self.assertEqual(expected_diff, git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo)) def testPreCommit_GitlinkWithoutDEPS(self): # Gitlink changes were staged without a corresponding DEPS change. self.write(self.repo, 'foo', 'foo') git.run('add', '.', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"a"*40},dep_b', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) _, stderr = git.run_with_stderr('commit', '-m', 'regular file and gitlinks', cwd=self.repo, env=self.env) self.assertIn('dep_a', diff_before_commit) self.assertIn('dep_b', diff_before_commit) # Gitlinks should be dropped. self.assertIn( 'Found no change to DEPS, but found staged gitlink(s) in diff', stderr) diff_after_commit = git.run('diff', '--name-only', 'HEAD^', 'HEAD', cwd=self.repo) self.assertNotIn('dep_a', diff_after_commit) self.assertNotIn('dep_b', diff_after_commit) self.assertIn('foo', diff_after_commit) def testPreCommit_IntentionalGitlinkWithoutDEPS(self): # Intentional Gitlink changes staged without a DEPS change. self.write(self.repo, 'foo', 'foo') git.run('add', '.', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"a"*40},dep_b', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) self.env['TESTING_ANSWER'] = '' _, stderr = git.run_with_stderr('commit', '-m', 'regular file and gitlinks', cwd=self.repo, env=self.env) self.assertIn('dep_a', diff_before_commit) self.assertIn('dep_b', diff_before_commit) # Gitlinks should be dropped. self.assertIn( 'Found no change to DEPS, but found staged gitlink(s) in diff', stderr) diff_after_commit = git.run('diff', '--name-only', 'HEAD^', 'HEAD', cwd=self.repo) self.assertIn('dep_a', diff_after_commit) self.assertIn('dep_b', diff_after_commit) self.assertIn('foo', diff_after_commit) def testPreCommit_OnlyGitlinkWithoutDEPS(self): # Gitlink changes were staged without a corresponding DEPS change but # no other files were included in the commit. git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) ret = git.run_with_retcode('commit', '-m', 'gitlink only', cwd=self.repo, env=self.env) self.assertIn('dep_a', diff_before_commit) # Gitlinks should be droppped and the empty commit should be aborted. self.assertEqual(ret, 1) diff_after_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) self.assertNotIn('dep_a', diff_after_commit) def testPreCommit_CommitAll(self): self.write(self.repo, 'foo', 'foo') git.run('add', '.', cwd=self.repo) git.run('commit', '-m', 'add foo', cwd=self.repo) self.write(self.repo, 'foo', 'foo2') # Create a new commit in dep_a. self.write(self.dep_a_repo, 'sub_foo', 'sub_foo') git.run('add', '.', cwd=self.dep_a_repo) git.run('commit', '-m', 'sub_foo', cwd=self.dep_a_repo) diff_before_commit = git.run('status', cwd=self.repo) self.assertIn('foo', diff_before_commit) self.assertIn('dep_a', diff_before_commit) ret = git.run_with_retcode('commit', '--all', '-m', 'commit all', cwd=self.repo, env=self.env) self.assertIn('dep_a', diff_before_commit) self.assertEqual(ret, 0) diff_after_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) self.assertNotIn('dep_a', diff_after_commit) diff_from_commit = git.run('diff', '--name-only', 'HEAD^', 'HEAD', cwd=self.repo) self.assertIn('foo', diff_from_commit) def testPreCommit_GitlinkWithDEPS(self): # A gitlink was staged with a corresponding DEPS change. updated_deps = '\n'.join(( f'git_dependencies = "{SYNC}"', 'deps = {', f' "dep_a": "host://dep_a@{"b"*40}",', f' "dep_b": "host://dep_b@{"b"*40}",', '}', )) self.write(self.repo, 'DEPS', updated_deps) git.run('add', '.', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', cwd=self.repo) git.run('commit', '-m', 'gitlink and DEPS', cwd=self.repo) # There should be no changes to the commit. diff_after_commit = git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo) self.assertEqual(diff_before_commit, diff_after_commit) def testPreCommit_SkipPrecommit(self): # A gitlink was staged without a corresponding DEPS change but the # SKIP_GITLINK_PRECOMMIT envvar was set. git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', '--name-only', cwd=self.repo) self.env['SKIP_GITLINK_PRECOMMIT'] = '1' git.run('commit', '-m', 'gitlink only, skipping precommit', cwd=self.repo, env=self.env) # Gitlink should be kept. self.assertIn('dep_a', diff_before_commit) diff_after_commit = git.run('diff', '--name-only', 'HEAD^', 'HEAD', cwd=self.repo) self.assertIn('dep_a', diff_after_commit) def testPreCommit_OtherDEPSState(self): # DEPS is set to a git_dependencies state other than SYNC. deps_content = '\n'.join(( f'git_dependencies = \'{SUBMODULES}\'', 'deps = {', f' "dep_a": "host://dep_a@{"a"*40}",', f' "dep_b": "host://dep_b@{"b"*40}",', '}', )) self.write(self.repo, 'DEPS', deps_content) git.run('add', '.', cwd=self.repo) git.run('commit', '-m', 'change git_dependencies', cwd=self.repo) git.run('update-index', '--replace', '--cacheinfo', f'160000,{"b"*40},dep_a', cwd=self.repo) diff_before_commit = git.run('diff', '--cached', cwd=self.repo) git.run('commit', '-m', 'update dep_a', cwd=self.repo) # There should be no changes to the commit. diff_after_commit = git.run('diff', 'HEAD^', 'HEAD', cwd=self.repo) self.assertEqual(diff_before_commit, diff_after_commit) if __name__ == '__main__': unittest.main()