e264112833cc draft — Jelmer Vernooij 9 months ago
Merge branch 'commit-no-verify' of git://github.com/pmrowla/dulwich
3 files changed, 61 insertions(+), 6 deletions(-)

M dulwich/porcelain.py
M dulwich/repo.py
M dulwich/tests/test_porcelain.py
M dulwich/porcelain.py +4 -2
@@ 323,7 323,8 @@ def symbolic_ref(repo, ref_name, force=F
         repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path)
 
 
-def commit(repo=".", message=None, author=None, committer=None, encoding=None):
+def commit(repo=".", message=None, author=None, committer=None, encoding=None,
+           no_verify=False):
     """Create a new commit.
 
     Args:

          
@@ 331,6 332,7 @@ def commit(repo=".", message=None, autho
       message: Optional commit message
       author: Optional author name and email
       committer: Optional committer name and email
+      no_verify: Skip pre-commit and commit-msg hooks
     Returns: SHA1 of the new commit
     """
     # FIXME: Support --all argument

          
@@ 344,7 346,7 @@ def commit(repo=".", message=None, autho
     with open_repo_closing(repo) as r:
         return r.do_commit(
                 message=message, author=author, committer=committer,
-                encoding=encoding)
+                encoding=encoding, no_verify=no_verify)
 
 
 def commit_tree(repo, tree, message=None, author=None, committer=None):

          
M dulwich/repo.py +9 -4
@@ 822,7 822,7 @@ class BaseRepo(object):
                   author=None, commit_timestamp=None,
                   commit_timezone=None, author_timestamp=None,
                   author_timezone=None, tree=None, encoding=None,
-                  ref=b'HEAD', merge_heads=None):
+                  ref=b'HEAD', merge_heads=None, no_verify=False):
         """Create a new commit.
 
         If not specified, `committer` and `author` default to

          
@@ 844,6 844,7 @@ class BaseRepo(object):
           encoding: Encoding
           ref: Optional ref to commit to (defaults to current branch)
           merge_heads: Merge heads (defaults to .git/MERGE_HEADS)
+          no_verify: Skip pre-commit and commit-msg hooks
 
         Returns:
           New commit SHA1

          
@@ 859,7 860,8 @@ class BaseRepo(object):
             c.tree = tree
 
         try:
-            self.hooks['pre-commit'].execute()
+            if not no_verify:
+                self.hooks['pre-commit'].execute()
         except HookError as e:
             raise CommitError(e)
         except KeyError:  # no hook defined, silent fallthrough

          
@@ 903,9 905,12 @@ class BaseRepo(object):
             raise ValueError("No commit message specified")
 
         try:
-            c.message = self.hooks['commit-msg'].execute(message)
-            if c.message is None:
+            if no_verify:
                 c.message = message
+            else:
+                c.message = self.hooks['commit-msg'].execute(message)
+                if c.message is None:
+                    c.message = message
         except HookError as e:
             raise CommitError(e)
         except KeyError:  # no hook defined, message not modified

          
M dulwich/tests/test_porcelain.py +48 -0
@@ 24,12 24,14 @@ from io import BytesIO, StringIO
 import os
 import re
 import shutil
+import stat
 import tarfile
 import tempfile
 import time
 
 from dulwich import porcelain
 from dulwich.diff_tree import tree_changes
+from dulwich.errors import CommitError
 from dulwich.objects import (
     Blob,
     Tag,

          
@@ 125,6 127,52 @@ class CommitTests(PorcelainTestCase):
         self.assertTrue(isinstance(sha, bytes))
         self.assertEqual(len(sha), 40)
 
+    def test_no_verify(self):
+        if os.name != 'posix':
+            self.skipTest('shell hook tests requires POSIX shell')
+        self.assertTrue(os.path.exists('/bin/sh'))
+
+        hooks_dir = os.path.join(self.repo.controldir(), "hooks")
+        os.makedirs(hooks_dir, exist_ok=True)
+        self.addCleanup(shutil.rmtree, hooks_dir)
+
+        c1, c2, c3 = build_commit_graph(
+                self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
+
+        hook_fail = "#!/bin/sh\nexit 1"
+
+        # hooks are executed in pre-commit, commit-msg order
+        # test commit-msg failure first, then pre-commit failure, then
+        # no_verify to skip both hooks
+        commit_msg = os.path.join(hooks_dir, "commit-msg")
+        with open(commit_msg, 'w') as f:
+            f.write(hook_fail)
+        os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
+
+        with self.assertRaises(CommitError):
+            porcelain.commit(
+                    self.repo.path, message="Some message",
+                    author="Joe <joe@example.com>",
+                    committer="Bob <bob@example.com>")
+
+        pre_commit = os.path.join(hooks_dir, "pre-commit")
+        with open(pre_commit, 'w') as f:
+            f.write(hook_fail)
+        os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
+
+        with self.assertRaises(CommitError):
+            porcelain.commit(
+                    self.repo.path, message="Some message",
+                    author="Joe <joe@example.com>",
+                    committer="Bob <bob@example.com>")
+
+        sha = porcelain.commit(
+                self.repo.path, message="Some message",
+                author="Joe <joe@example.com>",
+                committer="Bob <bob@example.com>", no_verify=True)
+        self.assertTrue(isinstance(sha, bytes))
+        self.assertEqual(len(sha), 40)
+
 
 class CleanTests(PorcelainTestCase):