summaryrefslogtreecommitdiffstats
path: root/qa/test_hooks.py
blob: 99e76ddc3d10a9100708117e24ca5e36bc6dd761 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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 <tmpdir>
            cd <tmpdir>
            git worktree add <worktree-tempdir>
            cd <worktree-tempdir>
            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)