diff --git a/dvc/exceptions.py b/dvc/exceptions.py index 0b992249e0..f7b6739a33 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -294,3 +294,12 @@ def __init__(self): "the given path is a DVC-file, you must specify a data file " "or a directory" ) + + +class GitHookAlreadyExistsError(DvcException): + def __init__(self, hook_name): + super(GitHookAlreadyExistsError, self).__init__( + "Hook '{}' already exists. Please refer to " + "https://man.dvc.org/install " + "for more info.".format(hook_name) + ) diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 2b9a679385..55bef9beb7 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -5,6 +5,9 @@ import os import logging +from funcy import cached_property + +from dvc.exceptions import GitHookAlreadyExistsError from dvc.utils.compat import str, open from dvc.utils import fix_env, relpath from dvc.scm.base import ( @@ -234,7 +237,7 @@ def _install_hook(self, name, cmd): " || exec dvc {}".format(cmd) ) - hook = os.path.join(self.root_dir, self.GIT_DIR, "hooks", name) + hook = self._hook_path(name) if os.path.isfile(hook): with open(hook, "r+") as fobj: @@ -247,6 +250,8 @@ def _install_hook(self, name, cmd): os.chmod(hook, 0o777) def install(self): + self._verify_dvc_hooks() + self._install_hook("post-checkout", "checkout") self._install_hook("pre-commit", "status") self._install_hook("pre-push", "push") @@ -342,3 +347,19 @@ def get_rev(self): def close(self): self.repo.close() + + @cached_property + def _hooks_home(self): + return os.path.join(self.root_dir, self.GIT_DIR, "hooks") + + def _hook_path(self, name): + return os.path.join(self._hooks_home, name) + + def _verify_hook(self, name): + if os.path.exists(self._hook_path(name)): + raise GitHookAlreadyExistsError(name) + + def _verify_dvc_hooks(self): + self._verify_hook("post-checkout") + self._verify_hook("pre-commit") + self._verify_hook("pre-push") diff --git a/tests/func/test_install.py b/tests/func/test_install.py index b4e07f4b86..73afdf28cd 100644 --- a/tests/func/test_install.py +++ b/tests/func/test_install.py @@ -2,6 +2,8 @@ import sys import pytest + +from dvc.exceptions import GitHookAlreadyExistsError from dvc.utils import file_md5 from dvc.main import main @@ -30,36 +32,12 @@ def test_should_create_hooks(self, git, dvc_repo): with open(self._hook(fname), "r") as fobj: assert command in fobj.read() - def test_should_append_hooks_if_file_already_exists(self, git, dvc_repo): + def test_should_fail_if_file_already_exists(self, git, dvc_repo): with open(self._hook("post-checkout"), "w") as fobj: - fobj.write("#!/bin/sh\n" "echo hello\n") - - assert main(["install"]) == 0 - - expected_script = ( - "#!/bin/sh\n" - "echo hello\n" - '[ "$3" = "0" ]' - ' || [ -z "$(git ls-files .dvc)" ]' - " || exec dvc checkout\n" - ) - - with open(self._hook("post-checkout"), "r") as fobj: - assert fobj.read() == expected_script - - def test_should_be_idempotent(self, git, dvc_repo): - assert main(["install"]) == 0 - assert main(["install"]) == 0 - - expected_script = ( - "#!/bin/sh\n" - '[ "$3" = "0" ]' - ' || [ -z "$(git ls-files .dvc)" ]' - " || exec dvc checkout\n" - ) + fobj.write("hook content") - with open(self._hook("post-checkout"), "r") as fobj: - assert fobj.read() == expected_script + with pytest.raises(GitHookAlreadyExistsError): + dvc_repo.scm.install() def test_should_post_checkout_hook_checkout(self, repo_dir, git, dvc_repo): assert main(["install"]) == 0