diff --git a/dvc/repo/experiments/executor/base.py b/dvc/repo/experiments/executor/base.py index 12b7a5495..8dba1681d 100644 --- a/dvc/repo/experiments/executor/base.py +++ b/dvc/repo/experiments/executor/base.py @@ -376,7 +376,7 @@ class BaseExecutor(ABC): logger.debug("Commit to new experiment branch '%s'", branch) scm.gitpython.repo.git.add(update=True) - scm.commit(f"dvc: commit experiment {exp_hash}") + scm.commit(f"dvc: commit experiment {exp_hash}", no_verify=True) new_rev = scm.get_rev() scm.set_ref(branch, new_rev, old_ref=old_ref) scm.set_ref(EXEC_BRANCH, branch, symbolic=True) diff --git a/dvc/scm/git/backend/base.py b/dvc/scm/git/backend/base.py index 3f61b41c3..e013184d4 100644 --- a/dvc/scm/git/backend/base.py +++ b/dvc/scm/git/backend/base.py @@ -56,7 +56,7 @@ class BaseGitBackend(ABC): pass @abstractmethod - def commit(self, msg: str): + def commit(self, msg: str, no_verify: bool = False): pass @abstractmethod diff --git a/dvc/scm/git/backend/dulwich.py b/dvc/scm/git/backend/dulwich.py index 68e7e3391..99aa03ceb 100644 --- a/dvc/scm/git/backend/dulwich.py +++ b/dvc/scm/git/backend/dulwich.py @@ -145,10 +145,14 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method # NOTE: this doesn't check gitignore, same as GitPythonBackend.add self.repo.stage(relpath(fpath, self.root_dir)) - def commit(self, msg: str): + def commit(self, msg: str, no_verify: bool = False): + from dulwich.errors import CommitError from dulwich.porcelain import commit - commit(self.root_dir, message=msg) + try: + commit(self.root_dir, message=msg, no_verify=no_verify) + except CommitError as exc: + raise SCMError("Git commit failed") from exc def checkout( self, branch: str, create_new: Optional[bool] = False, **kwargs, diff --git a/dvc/scm/git/backend/gitpython.py b/dvc/scm/git/backend/gitpython.py index c5b711b2d..88910a69f 100644 --- a/dvc/scm/git/backend/gitpython.py +++ b/dvc/scm/git/backend/gitpython.py @@ -200,8 +200,13 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method logger.exception(msg) - def commit(self, msg: str): - self.repo.index.commit(msg) + def commit(self, msg: str, no_verify: bool = False): + from git.exc import HookExecutionError + + try: + self.repo.index.commit(msg, skip_hooks=no_verify) + except HookExecutionError as exc: + raise SCMError("Git pre-commit hook failed") from exc def checkout( self, branch: str, create_new: Optional[bool] = False, **kwargs diff --git a/dvc/scm/git/backend/pygit2.py b/dvc/scm/git/backend/pygit2.py index 51c7bfe98..6e1190a81 100644 --- a/dvc/scm/git/backend/pygit2.py +++ b/dvc/scm/git/backend/pygit2.py @@ -86,7 +86,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method def add(self, paths: Iterable[str]): raise NotImplementedError - def commit(self, msg: str): + def commit(self, msg: str, no_verify: bool = False): raise NotImplementedError def checkout( diff --git a/setup.py b/setup.py index fede46905..8b28575fb 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ install_requires = [ "colorama>=0.3.9", "configobj>=5.0.6", "gitpython>3", - "dulwich>=0.20.14", + "dulwich>=0.20.15", "pygit2>=1.4.0", "setuptools>=34.0.0", "nanotime>=0.5.2", diff --git a/tests/unit/scm/test_git.py b/tests/unit/scm/test_git.py index edcbc4413..fee188cb2 100644 --- a/tests/unit/scm/test_git.py +++ b/tests/unit/scm/test_git.py @@ -301,3 +301,23 @@ def test_ignore_remove_empty(tmp_dir, scm, git): git.ignore_remove(test_entries[1]["path"]) assert not path_to_gitignore.exists() + + +@pytest.mark.skipif( + os.name == "nt", reason="Git hooks not supported on Windows" +) +@pytest.mark.parametrize("hook", ["pre-commit", "commit-msg"]) +def test_commit_no_verify(tmp_dir, scm, git, hook): + import stat + + hook_file = os.path.join(".git", "hooks", hook) + tmp_dir.gen( + hook_file, "#!/usr/bin/env python\nimport sys\nsys.exit(1)", + ) + os.chmod(hook_file, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) + + tmp_dir.gen("foo", "foo") + git.add(["foo"]) + with pytest.raises(SCMError): + git.commit("commit foo") + git.commit("commit foo", no_verify=True)