Repository: pre-commit/pre-commit

Issue:
Hook fail_fast stops pre-commit if any prior hook fails
### search you tried in the issue tracker

fail fast hook

### describe your issue

#### Description

Hook `fail_fast` says in the documentation that pre-commit will stop if this hook fails, which suggests it is referring only to the present hook. The actual behaviour appears to be that pre-commit will stop if this *or any previous* hook fails.

#### Example

Add a Python file containing including a blank line at the end:

```python
def my_function(b=):
    a = "a"

```

Use the config below and call `pre-commit run -a`.

#### Expected behaviour

All three hooks run. The first and third fail.

#### Observed behaviour

The first two hooks run. The first hook fails and the second hook passes.

#### Note

I had previously submitted this as a documentation issue (https://github.com/pre-commit/pre-commit.com/issues/945), because I find the actual behaviour quite useful, as it means we can avoid running a slow hook (e.g. pytest) if any other hook has failed. However, @asottile said this is not the intended behaviour, so resubmitting here as a bug. 

### pre-commit --version

pre-commit 3.6.2

### .pre-commit-config.yaml

```yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-ast
      - id: end-of-file-fixer
        fail_fast: true
      - id: double-quote-string-fixer
```


### ~/.cache/pre-commit/pre-commit.log (if present)

Log from terminal, not file.

```
> pre-commit run -a
check python ast.........................................................Failed
- hook id: check-ast
- exit code: 1

file1.py: failed parsing with CPython 3.11.7:

    Traceback (most recent call last):
      File "<path_redacted>\check_ast.py", line 21, in main
        ast.parse(f.read(), filename=filename)
      File "C:\Program Files\Python311\Lib\ast.py", line 50, in parse
        return compile(source, filename, mode, flags,
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "file1.py", line 1
        def my_function(b=):
                         ^
    SyntaxError: expected default value expression

fix end of files.........................................................Passed
```

Discussion:

Patch:
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
index 076f16d..2a08dff 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -298,7 +298,7 @@ def _run_hooks(
             verbose=args.verbose, use_color=args.color,
         )
         retval |= current_retval
-        if retval and (config['fail_fast'] or hook.fail_fast):
+        if current_retval and (config['fail_fast'] or hook.fail_fast):
             break
     if retval and args.show_diff_on_failure and prior_diff:
         if args.all_files:


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
index e36a3ca..50a20f3 100644
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -1088,6 +1088,22 @@ def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook):
     assert printed.count(b'Failing hook') == 1
 
 
+def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook):
+    with modify_config() as config:
+        config['repos'].append({
+            'repo': 'meta',
+            'hooks': [
+                {'id': 'identity', 'fail_fast': True},
+                {'id': 'identity', 'name': 'run me!'},
+            ],
+        })
+    stage_a_file()
+
+    ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts())
+    # should still run the last hook since the `fail_fast` one didn't fail
+    assert printed.count(b'run me!') == 1
+
+
 def test_classifier_removes_dne():
     classifier = Classifier(('this_file_does_not_exist',))
     assert classifier.filenames == []