nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: pre-commit/pre-commit
Pull request #654 (2017-11-03 16:30:25)

always_run hooks always apply to the repository
CC @hackedd, just a tiny edge case I thought of while writing up the changelog


Patch:
diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py
index 8c9a92d..20d7f06 100644
--- a/pre_commit/meta_hooks/check_hooks_apply.py
+++ b/pre_commit/meta_hooks/check_hooks_apply.py
@@ -14,6 +14,8 @@ def check_all_hooks_match_files(config_file):
 
     for repo in runner.repositories:
         for hook_id, hook in repo.hooks:
+            if hook['always_run']:
+                continue
             include, exclude = hook['files'], hook['exclude']
             filtered = _filter_by_include_exclude(files, include, exclude)
             types, exclude_types = hook['types'], hook['exclude_types']


Test patch:
diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py
index 0ca6880..86bc598 100644
--- a/tests/meta_hooks/check_hooks_apply_test.py
+++ b/tests/meta_hooks/check_hooks_apply_test.py
@@ -116,6 +116,12 @@ def test_valid_includes(
                 OrderedDict((
                     ('id', 'check-useless-excludes'),
                 )),
+                # Should not be reported as an error due to always_run
+                OrderedDict((
+                    ('id', 'check-useless-excludes'),
+                    ('files', '^$'),
+                    ('always_run', True),
+                )),
             ),
         ),
     ))
Repository: pre-commit/pre-commit
Pull request #661 (2017-11-08 23:51:40)

[Fix] Add existing repo_config to output_repos
Aims to fix https://github.com/pre-commit/pre-commit/issues/660


Patch:
diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py
index ca8f9bb..5ba5a8e 100644
--- a/pre_commit/commands/autoupdate.py
+++ b/pre_commit/commands/autoupdate.py
@@ -116,11 +116,12 @@ def autoupdate(runner, tags_only, repo=None):
     input_config = load_config(runner.config_file_path)
 
     for repo_config in input_config['repos']:
-        # Skip any repo_configs that aren't the specified repo
-        if repo and repo != repo_config['repo']:
-            continue
-
-        if is_local_repo(repo_config) or is_meta_repo(repo_config):
+        if (
+            is_local_repo(repo_config) or
+            is_meta_repo(repo_config) or
+            # Skip updating any repo_configs that aren't for the specified repo
+            repo and repo != repo_config['repo']
+        ):
             output_repos.append(repo_config)
             continue
         output.write('Updating {}...'.format(repo_config['repo']))


Test patch:
diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py
index c78af1f..ee20c7d 100644
--- a/tests/commands/autoupdate_test.py
+++ b/tests/commands/autoupdate_test.py
@@ -127,10 +127,12 @@ def test_autoupdate_out_of_date_repo(
 def test_autoupdate_out_of_date_repo_with_correct_repo_name(
         out_of_date_repo, in_tmpdir, mock_out_store_directory,
 ):
-    # Write out the config
-    config = make_config_from_repo(
+    stale_config = make_config_from_repo(
         out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False,
     )
+    local_config = config_with_local_hooks()
+    config = {'repos': [stale_config, local_config]}
+    # Write out the config
     write_config('.', config)
 
     runner = Runner('.', C.CONFIG_FILE)
@@ -141,6 +143,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name(
     assert ret == 0
     assert before != after
     assert out_of_date_repo.head_sha in after
+    assert local_config['repo'] in after
 
 
 def test_autoupdate_out_of_date_repo_with_wrong_repo_name(
Repository: pre-commit/pre-commit
Pull request #692 (2018-01-24 14:02:47)

Properly detect if commit is a root commit
Fix bad check for ancestor root commits.


Patch:
diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl
index f866eef..0a3dad5 100644
--- a/pre_commit/resources/pre-push-tmpl
+++ b/pre_commit/resources/pre-push-tmpl
@@ -8,7 +8,8 @@ do
             if [ -n "$first_ancestor" ]; then
                 # Check that the ancestor has at least one parent
                 git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null
-                if [ $? -ne 0 ]; then
+                if [ $? -eq 0 ]; then
+                    # Pushing the whole tree, including the root commit, so run on all files
                     args="--all-files"
                 else
                     source=$(git rev-parse "$first_ancestor"^)


Test patch:
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 2ba5ce3..1659684 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -564,6 +564,23 @@ def test_pre_push_integration_accepted(tempdir_factory):
         assert 'Passed' in output
 
 
+def test_pre_push_new_upstream(tempdir_factory):
+    upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
+    upstream2 = git_dir(tempdir_factory)
+    path = tempdir_factory.get()
+    cmd_output('git', 'clone', upstream, path)
+    with cwd(path):
+        install(Runner(path, C.CONFIG_FILE), hook_type='pre-push')
+        assert _get_commit_output(tempdir_factory)[0] == 0
+
+        cmd_output('git', 'remote', 'rename', 'origin', 'upstream')
+        cmd_output('git', 'remote', 'add', 'origin', upstream2)
+        retc, output = _get_push_output(tempdir_factory)
+        assert retc == 0
+        assert 'Bash hook' in output
+        assert 'Passed' in output
+
+
 def test_pre_push_integration_empty_push(tempdir_factory):
     upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
     path = tempdir_factory.get()
Repository: pre-commit/pre-commit
Pull request #694 (2018-01-30 05:48:02)

Fix legacy commit-msg hooks


Patch:
diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl
index ded311c..b7f1623 100644
--- a/pre_commit/resources/hook-tmpl
+++ b/pre_commit/resources/hook-tmpl
@@ -27,7 +27,7 @@ else
 fi
 
 # Run the legacy pre-commit if it exists
-if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy; then
+if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then
     retv=1
 fi
 


Test patch:
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 1659684..1469a3e 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -611,6 +611,30 @@ def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory):
     assert first_line.endswith('...Passed')
 
 
+def test_commit_msg_legacy(commit_msg_repo, tempdir_factory):
+    runner = Runner(commit_msg_repo, C.CONFIG_FILE)
+
+    hook_path = runner.get_hook_path('commit-msg')
+    mkdirp(os.path.dirname(hook_path))
+    with io.open(hook_path, 'w') as hook_file:
+        hook_file.write(
+            '#!/usr/bin/env bash\n'
+            'set -eu\n'
+            'test -e "$1"\n'
+            'echo legacy\n',
+        )
+    make_executable(hook_path)
+
+    install(runner, hook_type='commit-msg')
+
+    msg = 'Hi\nSigned off by: asottile'
+    retc, out = _get_commit_output(tempdir_factory, commit_msg=msg)
+    assert retc == 0
+    first_line, second_line = out.splitlines()[:2]
+    assert first_line == 'legacy'
+    assert second_line.startswith('Must have "Signed off by:"...')
+
+
 def test_install_disallow_mising_config(tempdir_factory):
     path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
     with cwd(path):
Repository: pre-commit/pre-commit
Pull request #729 (2018-03-18 03:04:06)

Fix regression: try-repo from relative path
This was broken in v1.7.0


Patch:
diff --git a/pre_commit/store.py b/pre_commit/store.py
index f5a9c25..8251e21 100644
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -143,10 +143,12 @@ class Store(object):
         def clone_strategy(directory):
             env = no_git_env()
 
+            cmd = ('git', 'clone', '--no-checkout', repo, directory)
+            cmd_output(*cmd, env=env)
+
             def _git_cmd(*args):
                 return cmd_output('git', *args, cwd=directory, env=env)
 
-            _git_cmd('clone', '--no-checkout', repo, '.')
             _git_cmd('reset', ref, '--hard')
             _git_cmd('submodule', 'update', '--init', '--recursive')
 


Test patch:
diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py
index 4fb0755..490cdd5 100644
--- a/tests/commands/try_repo_test.py
+++ b/tests/commands/try_repo_test.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+import os.path
 import re
 
 from pre_commit.commands.try_repo import try_repo
@@ -69,3 +70,13 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory):
         config,
     )
     assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n'  # noqa
+
+
+def test_try_repo_relative_path(cap_out, tempdir_factory):
+    repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo')
+    with cwd(git_dir(tempdir_factory)):
+        open('test-file', 'a').close()
+        cmd_output('git', 'add', '.')
+        relative_repo = os.path.relpath(repo, '.')
+        # previously crashed on cloning a relative path
+        assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook'))
Repository: pre-commit/pre-commit
Pull request #767 (2018-06-11 19:50:55)

Consistent ordering of filenames
Apparently `mypy` will return different results depending on the ordering of files.  This makes that ordering more consistent.


Patch:
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
index 98ae25d..a072566 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -38,14 +38,14 @@ def _hook_msg_start(hook, verbose):
 
 def _filter_by_include_exclude(filenames, include, exclude):
     include_re, exclude_re = re.compile(include), re.compile(exclude)
-    return {
+    return [
         filename for filename in filenames
         if (
             include_re.search(filename) and
             not exclude_re.search(filename) and
             os.path.lexists(filename)
         )
-    }
+    ]
 
 
 def _filter_by_types(filenames, types, exclude_types):


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
index cd32c5f..91e84d9 100644
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -770,19 +770,19 @@ def test_fail_fast(
 def some_filenames():
     return (
         '.pre-commit-hooks.yaml',
-        'pre_commit/main.py',
-        'pre_commit/git.py',
         'im_a_file_that_doesnt_exist.py',
+        'pre_commit/git.py',
+        'pre_commit/main.py',
     )
 
 
 def test_include_exclude_base_case(some_filenames):
     ret = _filter_by_include_exclude(some_filenames, '', '^$')
-    assert ret == {
+    assert ret == [
         '.pre-commit-hooks.yaml',
-        'pre_commit/main.py',
         'pre_commit/git.py',
-    }
+        'pre_commit/main.py',
+    ]
 
 
 @xfailif_no_symlink
@@ -790,19 +790,19 @@ def test_matches_broken_symlink(tmpdir):  # pragma: no cover (non-windows)
     with tmpdir.as_cwd():
         os.symlink('does-not-exist', 'link')
         ret = _filter_by_include_exclude({'link'}, '', '^$')
-        assert ret == {'link'}
+        assert ret == ['link']
 
 
 def test_include_exclude_total_match(some_filenames):
     ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$')
-    assert ret == {'pre_commit/main.py', 'pre_commit/git.py'}
+    assert ret == ['pre_commit/git.py', 'pre_commit/main.py']
 
 
 def test_include_exclude_does_search_instead_of_match(some_filenames):
     ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$')
-    assert ret == {'.pre-commit-hooks.yaml'}
+    assert ret == ['.pre-commit-hooks.yaml']
 
 
 def test_include_exclude_exclude_removes_files(some_filenames):
     ret = _filter_by_include_exclude(some_filenames, '', r'\.py$')
-    assert ret == {'.pre-commit-hooks.yaml'}
+    assert ret == ['.pre-commit-hooks.yaml']
Repository: pre-commit/pre-commit
Pull request #809 (2018-08-06 16:27:13)

Support `pre-commit install` inside a worktree


Patch:
diff --git a/pre_commit/git.py b/pre_commit/git.py
index 4fb2e65..d9e01f5 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -31,10 +31,16 @@ def get_root():
 
 
 def get_git_dir(git_root):
-    return os.path.normpath(os.path.join(
-        git_root,
-        cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(),
-    ))
+    def _git_dir(opt):
+        return os.path.normpath(os.path.join(
+            git_root,
+            cmd_output('git', 'rev-parse', opt, cwd=git_root)[1].strip(),
+        ))
+
+    try:
+        return _git_dir('--git-common-dir')
+    except CalledProcessError:  # pragma: no cover (git < 2.5)
+        return _git_dir('--git-dir')
 
 
 def get_remote_url(git_root):


Test patch:
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 7345cfb..e6f0e41 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -172,6 +172,19 @@ def test_install_in_submodule_and_run(tempdir_factory, store):
         assert NORMAL_PRE_COMMIT_RUN.match(output)
 
 
+def test_install_in_worktree_and_run(tempdir_factory, store):
+    src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
+    path = tempdir_factory.get()
+    cmd_output('git', '-C', src_path, 'branch', '-m', 'notmaster')
+    cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master')
+
+    with cwd(path):
+        assert install(Runner(path, C.CONFIG_FILE), store) == 0
+        ret, output = _get_commit_output(tempdir_factory)
+        assert ret == 0
+        assert NORMAL_PRE_COMMIT_RUN.match(output)
+
+
 def test_commit_am(tempdir_factory, store):
     """Regression test for #322."""
     path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
Repository: pre-commit/pre-commit
Pull request #837 (2018-10-02 15:19:33)

Fix `check-useless-exclude` to consider types filter
Fixes https://github.com/pre-commit/pre-commit/issues/704


Patch:
diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py
index cdc556d..18b9f16 100644
--- a/pre_commit/meta_hooks/check_useless_excludes.py
+++ b/pre_commit/meta_hooks/check_useless_excludes.py
@@ -9,6 +9,7 @@ import pre_commit.constants as C
 from pre_commit import git
 from pre_commit.clientlib import load_config
 from pre_commit.clientlib import MANIFEST_HOOK_DICT
+from pre_commit.commands.run import _filter_by_types
 
 
 def exclude_matches_any(filenames, include, exclude):
@@ -39,8 +40,10 @@ def check_useless_excludes(config_file):
             # Not actually a manifest dict, but this more accurately reflects
             # the defaults applied during runtime
             hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
+            types, exclude_types = hook['types'], hook['exclude_types']
+            filtered_by_types = _filter_by_types(files, types, exclude_types)
             include, exclude = hook['files'], hook['exclude']
-            if not exclude_matches_any(files, include, exclude):
+            if not exclude_matches_any(filtered_by_types, include, exclude):
                 print(
                     'The exclude pattern {!r} for {} does not match any files'
                     .format(exclude, hook['id']),


Test patch:
diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py
index 1a03fb0..b2cc187 100644
--- a/tests/meta_hooks/check_useless_excludes_test.py
+++ b/tests/meta_hooks/check_useless_excludes_test.py
@@ -51,6 +51,37 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory):
     assert expected == out
 
 
+def test_useless_exclude_with_types_filter(capsys, tempdir_factory):
+    config = {
+        'repos': [
+            {
+                'repo': 'meta',
+                'hooks': [
+                    {
+                        'id': 'check-useless-excludes',
+                        'exclude': '.pre-commit-config.yaml',
+                        'types': ['python'],
+                    },
+                ],
+            },
+        ],
+    }
+
+    repo = git_dir(tempdir_factory)
+    add_config_to_repo(repo, config)
+
+    with cwd(repo):
+        assert check_useless_excludes.main(()) == 1
+
+    out, _ = capsys.readouterr()
+    out = out.strip()
+    expected = (
+        "The exclude pattern '.pre-commit-config.yaml' for "
+        "check-useless-excludes does not match any files"
+    )
+    assert expected == out
+
+
 def test_no_excludes(capsys, tempdir_factory):
     config = {
         'repos': [
Repository: pre-commit/pre-commit
Pull request #857 (2018-10-31 16:40:53)

Reraises InvalidManifestError as RepositoryCannotBeUpdatedError
For #856


Patch:
diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py
index 8f3714c..d93d7e1 100644
--- a/pre_commit/commands/autoupdate.py
+++ b/pre_commit/commands/autoupdate.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 import re
 from collections import OrderedDict
 
+import six
 from aspy.yaml import ordered_dump
 from aspy.yaml import ordered_load
 from cfgv import remove_defaults
@@ -11,6 +12,7 @@ from cfgv import remove_defaults
 import pre_commit.constants as C
 from pre_commit import output
 from pre_commit.clientlib import CONFIG_SCHEMA
+from pre_commit.clientlib import InvalidManifestError
 from pre_commit.clientlib import is_local_repo
 from pre_commit.clientlib import is_meta_repo
 from pre_commit.clientlib import load_config
@@ -53,11 +55,15 @@ def _update_repo(repo_config, store, tags_only):
     # Construct a new config with the head rev
     new_config = OrderedDict(repo_config)
     new_config['rev'] = rev
-    new_repo = Repository.create(new_config, store)
+
+    try:
+        new_hooks = Repository.create(new_config, store).manifest_hooks
+    except InvalidManifestError as e:
+        raise RepositoryCannotBeUpdatedError(six.text_type(e))
 
     # See if any of our hooks were deleted with the new commits
     hooks = {hook['id'] for hook in repo_config['hooks']}
-    hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks))
+    hooks_missing = hooks - set(new_hooks)
     if hooks_missing:
         raise RepositoryCannotBeUpdatedError(
             'Cannot update because the tip of master is missing these hooks:\n'


Test patch:
diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py
index 3bfb62e..b6e81b2 100644
--- a/tests/commands/autoupdate_test.py
+++ b/tests/commands/autoupdate_test.py
@@ -260,6 +260,21 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store):
         assert 'v1.2.3' in f.read()
 
 
+def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store):
+    config = make_config_from_repo(
+        out_of_date_repo.path, rev=out_of_date_repo.original_rev,
+    )
+    write_config('.', config)
+
+    cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/')
+    cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm')
+
+    ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False)
+    assert ret == 1
+    with open(C.CONFIG_FILE) as f:
+        assert out_of_date_repo.original_rev in f.read()
+
+
 @pytest.fixture
 def hook_disappearing_repo(tempdir_factory):
     path = make_repo(tempdir_factory, 'python_hooks_repo')
Repository: pre-commit/pre-commit
Pull request #870 (2018-11-15 22:17:29)

Upgrade the sample config


Patch:
diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py
index 87bcaa7..38320f6 100644
--- a/pre_commit/commands/sample_config.py
+++ b/pre_commit/commands/sample_config.py
@@ -12,7 +12,7 @@ SAMPLE_CONFIG = '''\
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v1.4.0
+    rev: v2.0.0
     hooks:
     -   id: trailing-whitespace
     -   id: end-of-file-fixer


Test patch:
diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py
index cd43d45..83942a4 100644
--- a/tests/commands/sample_config_test.py
+++ b/tests/commands/sample_config_test.py
@@ -13,7 +13,7 @@ def test_sample_config(capsys):
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v1.4.0
+    rev: v2.0.0
     hooks:
     -   id: trailing-whitespace
     -   id: end-of-file-fixer
Repository: pre-commit/pre-commit
Pull request #1030 (2019-05-11 19:19:51)

Improve output when interrupted (^C)
Extracted the parts I'm more sure about from #1027


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bb875ce..ad7bf01 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,7 @@
   - ruby + gem
   - docker
 
-### Setting up an environment
+### Setting up an environemnt
 
 This is useful for running specific tests.  The easiest way to set this up
 is to run:
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
index 3b0a4c5..946f134 100644
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -44,9 +44,11 @@ def _log_and_exit(msg, exc, formatted):
 def error_handler():
     try:
         yield
-    except FatalError as e:
-        _log_and_exit('An error has occurred', e, traceback.format_exc())
-    except Exception as e:
-        _log_and_exit(
-            'An unexpected error has occurred', e, traceback.format_exc(),
-        )
+    except (Exception, KeyboardInterrupt) as e:
+        if isinstance(e, FatalError):
+            msg = 'An error has occurred'
+        elif isinstance(e, KeyboardInterrupt):
+            msg = 'Interrupted (^C)'
+        else:
+            msg = 'An unexpected error has occurred'
+        _log_and_exit(msg, e, traceback.format_exc())


Test patch:
diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py
index 6aebe5a..1b222f9 100644
--- a/tests/error_handler_test.py
+++ b/tests/error_handler_test.py
@@ -73,6 +73,29 @@ def test_error_handler_uncaught_error(mocked_log_and_exit):
     )
 
 
+def test_error_handler_keyboardinterrupt(mocked_log_and_exit):
+    exc = KeyboardInterrupt()
+    with error_handler.error_handler():
+        raise exc
+
+    mocked_log_and_exit.assert_called_once_with(
+        'Interrupted (^C)',
+        exc,
+        # Tested below
+        mock.ANY,
+    )
+    assert re.match(
+        r'Traceback \(most recent call last\):\n'
+        r'  File ".+pre_commit.error_handler.py", line \d+, in error_handler\n'
+        r'    yield\n'
+        r'  File ".+tests.error_handler_test.py", line \d+, '
+        r'in test_error_handler_keyboardinterrupt\n'
+        r'    raise exc\n'
+        r'KeyboardInterrupt\n',
+        mocked_log_and_exit.call_args[0][2],
+    )
+
+
 def test_log_and_exit(cap_out, mock_store_dir):
     with pytest.raises(SystemExit):
         error_handler._log_and_exit(