import os from qa.base import BaseTestCase from qa.shell import git, gitlint class HookTests(BaseTestCase): """Integration tests for gitlint commitmsg hooks""" VIOLATIONS = [ "gitlint: checking commit message...\n", '1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n', "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This ïs a title.\"\n", '2: B4 Second line is not empty: "Contënt on the second line"\n', "3: B6 Body message is missing\n", "-----------------------------------------------\n", "gitlint: \x1b[31mYour commit message contains violations.\x1b[0m\n", ] def setUp(self): super().setUp() self.responses = [] self.response_index = 0 self.githook_output = [] # The '--staged' flag used in the commit-msg hook fetches additional information from the underlying # git repo which means there already needs to be a commit in the repo # (as gitlint --staged doesn't work against empty repos) self.create_simple_commit("Commït Title\n\nCommit Body explaining commit.") # install git commit-msg hook and assert output output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo) commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") expected_installed = f"Successfully installed gitlint commit-msg hook in {commit_msg_hook_path}\n" self.assertEqualStdout(output_installed, expected_installed) def tearDown(self): # uninstall git commit-msg hook and assert output output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo) commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") expected_uninstalled = f"Successfully uninstalled gitlint commit-msg hook from {commit_msg_hook_path}\n" self.assertEqualStdout(output_uninstalled, expected_uninstalled) super().tearDown() def _violations(self): # Make a copy of the violations array so that we don't inadvertently edit it in the test (like I did :D) return list(self.VIOLATIONS) # callback function that captures git commit-msg hook output def _interact(self, line, stdin): self.githook_output.append(line) # Answer 'yes' to question to keep violating commit-msg if "Your commit message contains violations" in line: response = self.responses[self.response_index] stdin.put(f"{response}\n") self.response_index = (self.response_index + 1) % len(self.responses) def test_commit_hook_no_violations(self): test_filename = self.create_simple_commit( "This ïs a title\n\nBody contënt that should work", out=self._interact, tty_in=True ) short_hash = self.get_last_commit_short_hash() expected_output = [ "gitlint: checking commit message...\n", "gitlint: \x1b[32mOK\x1b[0m (no violations in commit message)\n", f"[main {short_hash}] This ïs a title\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n", f" create mode 100644 {test_filename}\n", ] for output, expected in zip(self.githook_output, expected_output): self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_continue(self): self.responses = ["y"] test_filename = self.create_simple_commit( "WIP: This ïs a title.\nContënt on the second line", out=self._interact, tty_in=True ) # Determine short commit-msg hash, needed to determine expected output short_hash = self.get_last_commit_short_hash() expected_output = self._violations() expected_output += [ "Continue with commit anyways (this keeps the current commit message)? " "[y(es)/n(no)/e(dit)] " f"[main {short_hash}] WIP: This ïs a title. Contënt on the second line\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n", f" create mode 100644 {test_filename}\n", ] for output, expected in zip(self.githook_output, expected_output): self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_abort(self): self.responses = ["n"] test_filename = self.create_simple_commit( "WIP: This ïs a title.\nContënt on the second line", out=self._interact, ok_code=1, tty_in=True ) git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) # Determine short commit-msg hash, needed to determine expected output expected_output = self._violations() expected_output += [ "Continue with commit anyways (this keeps the current commit message)? " "[y(es)/n(no)/e(dit)] " "Commit aborted.\n", "Your commit message: \n", "-----------------------------------------------\n", "WIP: This ïs a title.\n", "Contënt on the second line\n", "-----------------------------------------------\n", ] for output, expected in zip(self.githook_output, expected_output): self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_edit(self): self.responses = ["e", "y"] env = {"EDITOR": ":"} test_filename = self.create_simple_commit( "WIP: This ïs a title.\nContënt on the second line", out=self._interact, env=env, tty_in=True ) git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) short_hash = git("rev-parse", "--short", "HEAD", _cwd=self.tmp_git_repo, _tty_in=True).replace("\n", "") # Determine short commit-msg hash, needed to determine expected output expected_output = self._violations() expected_output += [ "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] " + self._violations()[0] ] expected_output += self._violations()[1:] expected_output += [ "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] " f"[main {short_hash}] WIP: This ïs a title. Contënt on the second line\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n", f" create mode 100644 {test_filename}\n", ] for output, expected in zip(self.githook_output, expected_output): self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_worktree(self): """Tests that hook installation and un-installation also work in git worktrees. Test steps: ```sh git init cd git worktree add cd gitlint install-hook gitlint uninstall-hook ``` """ tmp_git_repo = self.create_tmp_git_repo() self.create_simple_commit("Simple title\n\nContënt in the body", git_repo=tmp_git_repo) worktree_dir = self.generate_temp_path() self.tmp_git_repos.append(worktree_dir) # make sure we clean up the worktree afterwards git("worktree", "add", worktree_dir, _cwd=tmp_git_repo, _tty_in=True) output_installed = gitlint("install-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\n" self.assertEqualStdout(output_installed, expected_msg) output_uninstalled = gitlint("uninstall-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\n" self.assertEqualStdout(output_uninstalled, expected_msg)