diff options
author | Joris Roovers <joris.roovers@gmail.com> | 2019-06-19 18:38:28 +0200 |
---|---|---|
committer | Joris Roovers <joris.roovers@gmail.com> | 2019-06-19 18:41:51 +0200 |
commit | af74eb213b70007c9e03927b219bd263fad3119a (patch) | |
tree | 9924ce03f7eaf000e4256b28570c79692fbc4297 | |
parent | 73ed12de2bfc1dc680b140b9a0833dc1acb53a5a (diff) |
Support for (un)installing hooks in git worktrees
Gitlint no longer hardcodes the path of the commit-msg hook, but takes the
correct path from git itself. This fixes issues with using git worktrees.
This fixes #68.
-rw-r--r-- | gitlint/git.py | 9 | ||||
-rw-r--r-- | gitlint/hooks.py | 10 | ||||
-rw-r--r-- | gitlint/tests/test_cli.py | 19 | ||||
-rw-r--r-- | gitlint/tests/test_git.py | 19 | ||||
-rw-r--r-- | gitlint/tests/test_hooks.py | 60 | ||||
-rw-r--r-- | qa/base.py | 16 | ||||
-rw-r--r-- | qa/test_hooks.py | 31 |
7 files changed, 122 insertions, 42 deletions
diff --git a/gitlint/git.py b/gitlint/git.py index 76fe1b9..8602746 100644 --- a/gitlint/git.py +++ b/gitlint/git.py @@ -1,3 +1,5 @@ +import os + import arrow import sh # import exceptions separately, this makes it a little easier to mock them out in the unit tests @@ -56,6 +58,13 @@ def git_commentchar(repository_path=None): return ustr(commentchar).replace(u"\n", u"") +def git_hooks_dir(repository_path): + """ Determine hooks directory for a given target dir """ + hooks_dir = _git("rev-parse", "--git-path", "hooks", _cwd=repository_path) + hooks_dir = ustr(hooks_dir).replace(u"\n", u"") + return os.path.realpath(os.path.join(repository_path, hooks_dir)) + + class GitCommitMessage(object): """ Class representing a git commit message. A commit message consists of the following: - context: The `GitContext` this commit message is part of diff --git a/gitlint/hooks.py b/gitlint/hooks.py index e5748e8..fc4dc4e 100644 --- a/gitlint/hooks.py +++ b/gitlint/hooks.py @@ -4,10 +4,10 @@ import os import stat from gitlint.utils import DEFAULT_ENCODING +from gitlint.git import git_hooks_dir -COMMIT_MSG_HOOK_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files/commit-msg") -COMMIT_MSG_HOOK_DST_PATH = os.path.join(".git", "hooks", "commit-msg") -HOOKS_DIR_PATH = os.path.join(".git", "hooks") +COMMIT_MSG_HOOK_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files", "commit-msg") +COMMIT_MSG_HOOK_DST_PATH = "commit-msg" GITLINT_HOOK_IDENTIFIER = "### gitlint commit-msg hook start ###\n" @@ -20,12 +20,12 @@ class GitHookInstaller(object): @staticmethod def commit_msg_hook_path(lint_config): - return os.path.realpath(os.path.join(lint_config.target, COMMIT_MSG_HOOK_DST_PATH)) + return os.path.join(git_hooks_dir(lint_config.target), COMMIT_MSG_HOOK_DST_PATH) @staticmethod def _assert_git_repo(target): """ Asserts that a given target directory is a git repository """ - hooks_dir = os.path.realpath(os.path.join(target, HOOKS_DIR_PATH)) + hooks_dir = git_hooks_dir(target) if not os.path.isdir(hooks_dir): raise GitHookInstallerError(u"{0} is not a git repository.".format(target)) diff --git a/gitlint/tests/test_cli.py b/gitlint/tests/test_cli.py index a92a13d..319532f 100644 --- a/gitlint/tests/test_cli.py +++ b/gitlint/tests/test_cli.py @@ -447,11 +447,12 @@ class CLITests(BaseTestCase): self.assertEqual(result.exit_code, 0) @patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook') - def test_install_hook(self, install_hook): + @patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur")) + def test_install_hook(self, _, install_hook): """ Test for install-hook subcommand """ result = self.cli.invoke(cli.cli, ["install-hook"]) - expected_path = os.path.join(os.getcwd(), hooks.COMMIT_MSG_HOOK_DST_PATH) - expected = "Successfully installed gitlint commit-msg hook in {0}\n".format(expected_path) + expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH) + expected = u"Successfully installed gitlint commit-msg hook in {0}\n".format(expected_path) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, expected) expected_config = config.LintConfig() @@ -459,11 +460,12 @@ class CLITests(BaseTestCase): install_hook.assert_called_once_with(expected_config) @patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook') - def test_install_hook_target(self, install_hook): + @patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur")) + def test_install_hook_target(self, _, install_hook): """ Test for install-hook subcommand with a specific --target option specified """ # Specified target result = self.cli.invoke(cli.cli, ["--target", self.SAMPLES_DIR, "install-hook"]) - expected_path = os.path.realpath(os.path.join(self.SAMPLES_DIR, hooks.COMMIT_MSG_HOOK_DST_PATH)) + expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH) expected = "Successfully installed gitlint commit-msg hook in %s\n" % expected_path self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, expected) @@ -483,11 +485,12 @@ class CLITests(BaseTestCase): install_hook.assert_called_once_with(expected_config) @patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook') - def test_uninstall_hook(self, uninstall_hook): + @patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur")) + def test_uninstall_hook(self, _, uninstall_hook): """ Test for uninstall-hook subcommand """ result = self.cli.invoke(cli.cli, ["uninstall-hook"]) - expected_path = os.path.realpath(os.path.join(os.getcwd(), hooks.COMMIT_MSG_HOOK_DST_PATH)) - expected = "Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_path) + expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH) + expected = u"Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_path) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, expected) expected_config = config.LintConfig() diff --git a/gitlint/tests/test_git.py b/gitlint/tests/test_git.py index bb1ac2b..f64776f 100644 --- a/gitlint/tests/test_git.py +++ b/gitlint/tests/test_git.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import datetime +import os + import dateutil try: @@ -13,7 +15,8 @@ except ImportError: from sh import ErrorReturnCode, CommandNotFound from gitlint.tests.base import BaseTestCase -from gitlint.git import GitContext, GitCommit, GitCommitMessage, GitContextError, GitNotInstalledError, git_commentchar +from gitlint.git import GitContext, GitCommit, GitCommitMessage, GitContextError, \ + GitNotInstalledError, git_commentchar, git_hooks_dir class GitTests(BaseTestCase): @@ -375,7 +378,19 @@ class GitTests(BaseTestCase): self.assertEqual(git_commentchar(), u"ä") git.return_value = ';\n' - self.assertEqual(git_commentchar(), ';') + self.assertEqual(git_commentchar(os.path.join(u"/föo", u"bar")), ';') + + git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1], + _cwd=os.path.join(u"/föo", u"bar")) + + @patch("gitlint.git._git") + def test_git_hooks_dir(self, git): + hooks_dir = os.path.join(u"föo", ".git", "hooks") + git.return_value.__str__ = lambda _: hooks_dir + "\n" + git.return_value.__unicode__ = lambda _: hooks_dir + "\n" + self.assertEqual(git_hooks_dir(u"/blä"), os.path.join(u"/blä", hooks_dir)) + + git.assert_called_once_with("rev-parse", "--git-path", "hooks", _cwd=u"/blä") @patch("gitlint.git.git_commentchar") def test_commit_msg_custom_commentchar(self, patched): diff --git a/gitlint/tests/test_hooks.py b/gitlint/tests/test_hooks.py index ca487c9..4fd5836 100644 --- a/gitlint/tests/test_hooks.py +++ b/gitlint/tests/test_hooks.py @@ -16,11 +16,16 @@ from gitlint.hooks import GitHookInstaller, GitHookInstallerError, COMMIT_MSG_HO class HookTests(BaseTestCase): - def test_commit_msg_hook_path(self): + + @patch('gitlint.hooks.git_hooks_dir') + def test_commit_msg_hook_path(self, git_hooks_dir): + git_hooks_dir.return_value = os.path.join(u"/föo", u"bar") lint_config = LintConfig() lint_config.target = self.SAMPLES_DIR - expected_path = os.path.join(self.SAMPLES_DIR, COMMIT_MSG_HOOK_DST_PATH) + expected_path = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) path = GitHookInstaller.commit_msg_hook_path(lint_config) + + git_hooks_dir.assert_called_once_with(self.SAMPLES_DIR) self.assertEqual(path, expected_path) @staticmethod @@ -29,36 +34,41 @@ class HookTests(BaseTestCase): @patch('gitlint.hooks.shutil.copy') @patch('os.path.exists', return_value=False) @patch('os.path.isdir', return_value=True) - def test_install_commit_msg_hook(isdir, path_exists, copy, stat, chmod): + @patch('gitlint.hooks.git_hooks_dir') + def test_install_commit_msg_hook(git_hooks_dir, isdir, path_exists, copy, stat, chmod): lint_config = LintConfig() - lint_config.target = os.path.join(u"/foo", u"bår") - expected_dst = os.path.join(u"/foo", u"bår", COMMIT_MSG_HOOK_DST_PATH) + lint_config.target = os.path.join(u"/hür", u"dur") + git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks") + expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) GitHookInstaller.install_commit_msg_hook(lint_config) - isdir.assert_called_with(os.path.join(u"/foo", u"bår", ".git", "hooks")) + isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_called_once_with(expected_dst) copy.assert_called_once_with(COMMIT_MSG_HOOK_SRC_PATH, expected_dst) stat.assert_called_once_with(expected_dst) chmod.assert_called_once_with(expected_dst, ANY) + git_hooks_dir.assert_called_with(lint_config.target) @patch('gitlint.hooks.shutil.copy') @patch('os.path.exists', return_value=False) @patch('os.path.isdir', return_value=True) - def test_install_commit_msg_hook_negative(self, isdir, path_exists, copy): + @patch('gitlint.hooks.git_hooks_dir') + def test_install_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, copy): lint_config = LintConfig() - lint_config.target = os.path.join(u"/foo", u"bår") + lint_config.target = os.path.join(u"/hür", u"dur") + git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks") # mock that current dir is not a git repo isdir.return_value = False - expected_msg = u"{0} is not a git repository".format(os.path.join(u"/foo", u"bår")) + expected_msg = u"{0} is not a git repository".format(lint_config.target) with self.assertRaisesRegex(GitHookInstallerError, expected_msg): GitHookInstaller.install_commit_msg_hook(lint_config) - isdir.assert_called_with(os.path.join(u"/foo", u"bår", ".git", "hooks")) + isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_not_called() copy.assert_not_called() # mock that there is already a commit hook present isdir.return_value = True path_exists.return_value = True - expected_dst = os.path.join(u"/foo", u"bår", COMMIT_MSG_HOOK_DST_PATH) + expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) expected_msg = u"There is already a commit-msg hook file present in {0}.\n".format(expected_dst) + \ "gitlint currently does not support appending to an existing commit-msg file." with self.assertRaisesRegex(GitHookInstallerError, expected_msg): @@ -68,41 +78,47 @@ class HookTests(BaseTestCase): @patch('os.remove') @patch('os.path.exists', return_value=True) @patch('os.path.isdir', return_value=True) - def test_uninstall_commit_msg_hook(isdir, path_exists, remove): + @patch('gitlint.hooks.git_hooks_dir') + def test_uninstall_commit_msg_hook(git_hooks_dir, isdir, path_exists, remove): lint_config = LintConfig() - lint_config.target = os.path.join(u"/foo", u"bår") + git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks") + lint_config.target = os.path.join(u"/hür", u"dur") read_data = "#!/bin/sh\n" + GITLINT_HOOK_IDENTIFIER with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True): GitHookInstaller.uninstall_commit_msg_hook(lint_config) - expected_dst = os.path.join(u"/foo", u"bår", COMMIT_MSG_HOOK_DST_PATH) - isdir.assert_called_with(os.path.join(u"/foo", u"bår", ".git", "hooks")) + expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) + isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_called_once_with(expected_dst) remove.assert_called_with(expected_dst) + git_hooks_dir.assert_called_with(lint_config.target) @patch('os.remove') @patch('os.path.exists', return_value=True) @patch('os.path.isdir', return_value=True) - def test_uninstall_commit_msg_hook_negative(self, isdir, path_exists, remove): + @patch('gitlint.hooks.git_hooks_dir') + def test_uninstall_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, remove): lint_config = LintConfig() - lint_config.target = os.path.join(u"/foo", u"bår") + lint_config.target = os.path.join(u"/hür", u"dur") + git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks") + # mock that the current directory is not a git repo isdir.return_value = False - expected_msg = u"{0} is not a git repository".format(os.path.join(u"/foo", u"bår")) + expected_msg = u"{0} is not a git repository".format(lint_config.target) with self.assertRaisesRegex(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) - isdir.assert_called_with(os.path.join(u"/foo", u"bår", '.git/hooks')) + isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_not_called() remove.assert_not_called() # mock that there is no commit hook present isdir.return_value = True path_exists.return_value = False - expected_dst = os.path.join(u"/foo", u"bår", COMMIT_MSG_HOOK_DST_PATH) + expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) expected_msg = u"There is no commit-msg hook present in {0}.".format(expected_dst) with self.assertRaisesRegex(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) - isdir.assert_called_with(os.path.join(u"/foo", u"bår", '.git/hooks')) + isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_called_once_with(expected_dst) remove.assert_not_called() @@ -110,7 +126,7 @@ class HookTests(BaseTestCase): isdir.return_value = True path_exists.return_value = True read_data = "#!/bin/sh\nfoo" - expected_dst = os.path.join(u"/foo", u"bår", COMMIT_MSG_HOOK_DST_PATH) + expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) expected_msg = u"The commit-msg hook in {0} was not installed by gitlint ".format(expected_dst) + \ r"\(or it was modified\).\nUninstallation of 3th party or modified gitlint hooks " + \ "is not supported." @@ -62,20 +62,25 @@ class BaseTestCase(TestCase): @classmethod def setUpClass(cls): """ Sets up the integration tests by creating a new temporary git repository """ - cls._tmp_git_repos = [cls.create_tmp_git_repo()] - cls.tmp_git_repo = cls._tmp_git_repos[0] + cls.tmp_git_repos = [] + cls.tmp_git_repo = cls.create_tmp_git_repo() @classmethod def tearDownClass(cls): """ Cleans up the temporary git repositories """ - for repo in cls._tmp_git_repos: + for repo in cls.tmp_git_repos: rm("-rf", repo) @classmethod + def generate_temp_path(cls): + return os.path.realpath("/tmp/gitlint-test-{0}".format(datetime.now().strftime("%Y%m%d-%H%M%S-%f"))) + + @classmethod def create_tmp_git_repo(cls): """ Creates a temporary git repository and returns its directory path """ - tmp_git_repo = os.path.realpath("/tmp/gitlint-test-{0}".format( - datetime.now().strftime("%Y%m%d-%H%M%S-%f"))) + tmp_git_repo = cls.generate_temp_path() + cls.tmp_git_repos.append(tmp_git_repo) + git("init", tmp_git_repo) # configuring name and email is required in every git repot git("config", "user.name", "gitlint-test-user", _cwd=tmp_git_repo) @@ -89,6 +94,7 @@ class BaseTestCase(TestCase): # Git on mac doesn't like unicode characters by default, so we need to set this option # http://stackoverflow.com/questions/5581857/git-and-the-umlaut-problem-on-mac-os-x git("config", "core.precomposeunicode", "true", _cwd=tmp_git_repo) + return tmp_git_repo def _create_simple_commit(self, message, out=None, ok_code=None, env=None, git_repo=None, tty_in=False): diff --git a/qa/test_hooks.py b/qa/test_hooks.py index b36cde6..29080fc 100644 --- a/qa/test_hooks.py +++ b/qa/test_hooks.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-function-args,unexpected-keyword-arg +import os from sh import git, gitlint # pylint: disable=no-name-in-module from qa.base import BaseTestCase @@ -115,3 +116,33 @@ class HookTests(BaseTestCase): 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(u"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 = "Successfully installed gitlint commit-msg hook in {0}\n".format(expected_hook_path) + self.assertEqual(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 = "Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_hook_path) + self.assertEqual(output_uninstalled, expected_msg) |