nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: pre-commit/pre-commit
Pull request #3302 (2024-09-17 00:37:13)

migrate-config rewrites deprecated stages
part of #2732


Patch:
diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py
index cdce83f..ada094f 100644
--- a/pre_commit/commands/migrate_config.py
+++ b/pre_commit/commands/migrate_config.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import functools
+import itertools
 import textwrap
 from typing import Callable
 
@@ -49,6 +50,10 @@ def _preserve_style(n: ScalarNode, *, s: str) -> str:
     return f'{n.style}{s}{n.style}'
 
 
+def _fix_stage(n: ScalarNode) -> str:
+    return _preserve_style(n, s=f'pre-{n.value}')
+
+
 def _migrate_composed(contents: str) -> str:
     tree = yaml_compose(contents)
     rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = []
@@ -76,6 +81,22 @@ def _migrate_composed(contents: str) -> str:
         if node.value == 'python_venv':
             rewrites.append((node, python_venv_replace))
 
+    # stages rewrites
+    default_stages_matcher = (MappingValue('default_stages'), SequenceItem())
+    default_stages_match = match(tree, default_stages_matcher)
+    hook_stages_matcher = (
+        MappingValue('repos'),
+        SequenceItem(),
+        MappingValue('hooks'),
+        SequenceItem(),
+        MappingValue('stages'),
+        SequenceItem(),
+    )
+    hook_stages_match = match(tree, hook_stages_matcher)
+    for node in itertools.chain(default_stages_match, hook_stages_match):
+        if node.value in {'commit', 'push', 'merge-commit'}:
+            rewrites.append((node, _fix_stage))
+
     rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index)
 
     src_parts = []


Test patch:
diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py
index c563866..9ffae6e 100644
--- a/tests/commands/migrate_config_test.py
+++ b/tests/commands/migrate_config_test.py
@@ -213,6 +213,48 @@ repos:
     assert cfg.read_text() == expected
 
 
+def test_migrate_config_default_stages(tmp_path):
+    src = '''\
+default_stages: [commit, push, merge-commit, commit-msg]
+repos: []
+'''
+    expected = '''\
+default_stages: [pre-commit, pre-push, pre-merge-commit, commit-msg]
+repos: []
+'''
+    cfg = tmp_path.joinpath('cfg.yaml')
+    cfg.write_text(src)
+    assert migrate_config(str(cfg)) == 0
+    assert cfg.read_text() == expected
+
+
+def test_migrate_config_hook_stages(tmp_path):
+    src = '''\
+repos:
+-   repo: local
+    hooks:
+    -   id: example
+        name: example
+        entry: example
+        language: system
+        stages: ["commit", "push", "merge-commit", "commit-msg"]
+'''
+    expected = '''\
+repos:
+-   repo: local
+    hooks:
+    -   id: example
+        name: example
+        entry: example
+        language: system
+        stages: ["pre-commit", "pre-push", "pre-merge-commit", "commit-msg"]
+'''
+    cfg = tmp_path.joinpath('cfg.yaml')
+    cfg.write_text(src)
+    assert migrate_config(str(cfg)) == 0
+    assert cfg.read_text() == expected
+
+
 def test_migrate_config_invalid_yaml(tmpdir):
     contents = '['
     cfg = tmpdir.join(C.CONFIG_FILE)
Repository: pre-commit/pre-commit
Pull request #3311 (2024-09-30 22:09:16)

also apply sensible regex warning for `repo: meta`


Patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index a49465e..0127c4d 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -289,6 +289,8 @@ META_HOOK_DICT = cfgv.Map(
         item
         for item in MANIFEST_HOOK_DICT.items
     ),
+    OptionalSensibleRegexAtHook('files', cfgv.check_string),
+    OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
 )
 CONFIG_HOOK_DICT = cfgv.Map(
     'Hook', 'id',


Test patch:
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index eaa8a04..9d31d33 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -256,6 +256,24 @@ def test_validate_optional_sensible_regex_at_local_hook(caplog):
     ]
 
 
+def test_validate_optional_sensible_regex_at_meta_hook(caplog):
+    config_obj = {
+        'repo': 'meta',
+        'hooks': [{'id': 'identity', 'files': 'dir/*.py'}],
+    }
+
+    cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+    assert caplog.record_tuples == [
+        (
+            'pre_commit',
+            logging.WARNING,
+            "The 'files' field in hook 'identity' is a regex, not a glob "
+            "-- matching '/*' probably isn't what you want here",
+        ),
+    ]
+
+
 @pytest.mark.parametrize(
     ('regex', 'warning'),
     (
Repository: pre-commit/pre-commit
Pull request #3313 (2024-09-30 23:24:05)

add warning for deprecated stages values in `default_stages`


Patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index 4e0425c..f788507 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -125,6 +125,32 @@ class DeprecatedStagesWarning(NamedTuple):
         raise NotImplementedError
 
 
+class DeprecatedDefaultStagesWarning(NamedTuple):
+    key: str
+
+    def check(self, dct: dict[str, Any]) -> None:
+        if self.key not in dct:
+            return
+
+        val = dct[self.key]
+        cfgv.check_array(cfgv.check_any)(val)
+
+        legacy_stages = [stage for stage in val if stage in _STAGES]
+        if legacy_stages:
+            logger.warning(
+                f'top-level `default_stages` uses deprecated stage names '
+                f'({", ".join(legacy_stages)}) which will be removed in a '
+                f'future version.  '
+                f'run: `pre-commit migrate-config` to automatically fix this.',
+            )
+
+    def apply_default(self, dct: dict[str, Any]) -> None:
+        pass
+
+    def remove_default(self, dct: dict[str, Any]) -> None:
+        raise NotImplementedError
+
+
 MANIFEST_HOOK_DICT = cfgv.Map(
     'Hook', 'id',
 
@@ -398,6 +424,7 @@ CONFIG_SCHEMA = cfgv.Map(
         'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
     ),
     StagesMigration('default_stages', STAGES),
+    DeprecatedDefaultStagesWarning('default_stages'),
     cfgv.Optional('files', check_string_regex, ''),
     cfgv.Optional('exclude', check_string_regex, '^$'),
     cfgv.Optional('fail_fast', cfgv.check_bool, False),


Test patch:
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index 1335e08..7aa84af 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -335,6 +335,30 @@ def test_no_warning_for_non_deprecated_stages(caplog):
     assert caplog.record_tuples == []
 
 
+def test_warning_for_deprecated_default_stages(caplog):
+    cfg = {'default_stages': ['commit', 'push'], 'repos': []}
+
+    cfgv.validate(cfg, CONFIG_SCHEMA)
+
+    assert caplog.record_tuples == [
+        (
+            'pre_commit',
+            logging.WARNING,
+            'top-level `default_stages` uses deprecated stage names '
+            '(commit, push) which will be removed in a future version.  '
+            'run: `pre-commit migrate-config` to automatically fix this.',
+        ),
+    ]
+
+
+def test_no_warning_for_non_deprecated_default_stages(caplog):
+    cfg = {'default_stages': ['pre-commit', 'pre-push'], 'repos': []}
+
+    cfgv.validate(cfg, CONFIG_SCHEMA)
+
+    assert caplog.record_tuples == []
+
+
 @pytest.mark.parametrize(
     'manifest_obj',
     (
Repository: pre-commit/pre-commit
Pull request #3425 (2025-03-13 12:02:13)

fix: crash on ambiguous ref 'HEAD'
I lost the log when I faced this crash, but it was of the form:

```
CalledProcessError: ('git', 'diff', '--name-only', '--no-ext-diff', '-z', '-m', '1234abc', 'HEAD', 'MERGE_HEAD'): Ambiguous reference 'HEAD'
... something about prepending '--', like
git diff ref [ref2] -- [files]
```

Likely happened because there is a folder named `head` in the base commit SHA.

And this fixes the issue. If you have more information on how I can create a minimal reproduction of the bug and add it as a test, I'd be happy to.


Patch:
diff --git a/pre_commit/git.py b/pre_commit/git.py
index 19aac38..2f424f8 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -126,7 +126,7 @@ def get_conflicted_files() -> set[str]:
     merge_diff_filenames = zsplit(
         cmd_output(
             'git', 'diff', '--name-only', '--no-ext-diff', '-z',
-            '-m', tree_hash, 'HEAD', 'MERGE_HEAD',
+            '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
         )[1],
     )
     return set(merge_conflict_filenames) | set(merge_diff_filenames)


Test patch:
diff --git a/tests/git_test.py b/tests/git_test.py
index 93f5a1c..02b6ce3 100644
--- a/tests/git_test.py
+++ b/tests/git_test.py
@@ -141,6 +141,15 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict):
     assert ret == {'conflict_file'}
 
 
+def test_get_conflicted_files_with_file_named_head(in_merge_conflict):
+    resolve_conflict()
+    open('HEAD', 'w').close()
+    cmd_output('git', 'add', 'HEAD')
+
+    ret = set(git.get_conflicted_files())
+    assert ret == {'conflict_file', 'HEAD'}
+
+
 MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n"
 OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n'
Repository: pre-commit/pre-commit-hooks
Pull request #76 (2015-08-02 00:04:12)

detect_private_key: print *filenames*, not the key
Currently it prints *the entire content* of the problem file (which might be really long, e.g. a Python test file with embedded dummy key) but doesn't actually print the file name.


Patch:
diff --git a/hooks.yaml b/hooks.yaml
index 9282867..bd0e724 100644
--- a/hooks.yaml
+++ b/hooks.yaml
@@ -85,7 +85,7 @@
     description: Ensures that a file is either empty, or ends with one newline.
     entry: end-of-file-fixer
     language: python
-    files: \.(html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml|pp)$
+    files: \.(html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml)$
 -   id: flake8
     name: Flake8
     description: This hook runs flake8.
@@ -115,4 +115,4 @@
     description: This hook trims trailing whitespace.
     entry: trailing-whitespace-fixer
     language: python
-    files: \.(html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml|pp)$
+    files: \.(html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml)$
diff --git a/pre_commit_hooks/detect_private_key.py b/pre_commit_hooks/detect_private_key.py
index 782b680..1a4f323 100644
--- a/pre_commit_hooks/detect_private_key.py
+++ b/pre_commit_hooks/detect_private_key.py
@@ -3,6 +3,12 @@ from __future__ import print_function
 import argparse
 import sys
 
+BLACKLIST = [
+    b'BEGIN RSA PRIVATE KEY',
+    b'BEGIN DSA PRIVATE KEY',
+    b'BEGIN EC PRIVATE KEY',
+]
+
 
 def detect_private_key(argv=None):
     parser = argparse.ArgumentParser()
@@ -12,11 +18,10 @@ def detect_private_key(argv=None):
     private_key_files = []
 
     for filename in args.filenames:
-        content = open(filename, 'rb').read()
-        if b'BEGIN RSA PRIVATE KEY' in content:
-            private_key_files.append(content)
-        if b'BEGIN DSA PRIVATE KEY' in content:
-            private_key_files.append(content)
+        with open(filename, 'rb') as f:
+            content = f.read()
+            if any(line in content for line in BLACKLIST):
+                private_key_files.append(filename)
 
     if private_key_files:
         for private_key_file in private_key_files:


Test patch:
diff --git a/tests/detect_private_key_test.py b/tests/detect_private_key_test.py
index c912624..d976472 100644
--- a/tests/detect_private_key_test.py
+++ b/tests/detect_private_key_test.py
@@ -8,6 +8,7 @@ from pre_commit_hooks.detect_private_key import detect_private_key
 TESTS = (
     (b'-----BEGIN RSA PRIVATE KEY-----', 1),
     (b'-----BEGIN DSA PRIVATE KEY-----', 1),
+    (b'-----BEGIN EC PRIVATE KEY-----', 1),
     (b'ssh-rsa DATA', 0),
     (b'ssh-dsa DATA', 0),
     # Some arbitrary binary data
Repository: pre-commit/pre-commit-hooks
Pull request #79 (2015-08-17 19:52:56)

Sort requirements.txt case-insensitively
Every time I `pip freeze` to update my requirements.txt and then run pre-commit, I get diffs like:
```diff
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
+Django==1.8.3
+PyMySQL==0.6.6
+SQLAlchemy==1.0.8
 colorama==0.3.3
 cracklib==2.9.3
-Django==1.8.3
 dnspython3==1.12.0
 ecdsa==0.13
 gunicorn==19.3.0
@@ -10,7 +12,5 @@ paramiko==1.15.2
 pexpect==3.2
 pyasn1==0.1.8
 pycrypto==2.6.1
-PyMySQL==0.6.6
 pysnmp==4.2.5
 requests==2.7.0
-SQLAlchemy==1.0.8
```

IMO the pip behavior is more useful (putting capitalized deps at the top seems unhelpful, especially since they can appear lowercase or uppercase and still be correct). Plus this avoids unnecessary churn.

FWIW I think this is a behavior of newer versions of pip; old pip (1.5.6) seems to keep capitals at the top, while the latest doesn't.


Patch:
diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py
index cc09242..bf7f75d 100644
--- a/pre_commit_hooks/requirements_txt_fixer.py
+++ b/pre_commit_hooks/requirements_txt_fixer.py
@@ -19,8 +19,8 @@ class Requirement(object):
             return False
         else:
             return (
-                self.value.partition(b'==') <
-                requirement.value.partition(b'==')
+                self.value.lower().partition(b'==') <
+                requirement.value.lower().partition(b'==')
             )
 
 


Test patch:
diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py
index 3eac637..3680733 100644
--- a/tests/requirements_txt_fixer_test.py
+++ b/tests/requirements_txt_fixer_test.py
@@ -16,6 +16,7 @@ TESTS = (
     (b'\nfoo\nbar\n', 1, b'bar\n\nfoo\n'),
     (b'\nbar\nfoo\n', 0, b'\nbar\nfoo\n'),
     (b'pyramid==1\npyramid-foo==2\n', 0, b'pyramid==1\npyramid-foo==2\n'),
+    (b'ocflib\nDjango\nPyMySQL\n', 1, b'Django\nocflib\nPyMySQL\n'),
 )
Repository: pre-commit/pre-commit-hooks
Pull request #98 (2016-01-14 23:26:26)

New hook tests for broken symlinks
Test for broken symlinks


Patch:
diff --git a/pre_commit_hooks/check_symlinks.py b/pre_commit_hooks/check_symlinks.py
new file mode 100644
index 0000000..d0f0f09
--- /dev/null
+++ b/pre_commit_hooks/check_symlinks.py
@@ -0,0 +1,26 @@
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import argparse
+import os
+import os.path
+
+
+def check_symlinks(argv=None):
+    parser = argparse.ArgumentParser(description='Checks for broken symlinks.')
+    parser.add_argument('filenames', nargs='*', help='Filenames to check')
+    args = parser.parse_args(argv)
+
+    retv = 0
+
+    for filename in args.filenames:
+        if os.path.islink(filename) and not os.path.exists(filename):
+            print('{0}: Broken symlink'.format(filename))
+            retv = 1
+
+    return retv
+
+
+if __name__ == '__main__':
+    exit(check_symlinks())


Test patch:
diff --git a/testing/resources/broken_symlink b/testing/resources/broken_symlink
new file mode 120000
index 0000000..ee1f6cb
--- /dev/null
+++ b/testing/resources/broken_symlink
@@ -0,0 +1,1 @@
+does_not_exist
\ No newline at end of file
diff --git a/testing/resources/does_exist b/testing/resources/does_exist
new file mode 100644
index 0000000..e69de29
diff --git a/testing/resources/working_symlink b/testing/resources/working_symlink
new file mode 120000
index 0000000..20b5061
--- /dev/null
+++ b/testing/resources/working_symlink
@@ -0,0 +1,1 @@
+does_exist
\ No newline at end of file
diff --git a/tests/check_symlinks_test.py b/tests/check_symlinks_test.py
new file mode 100644
index 0000000..4b11e71
--- /dev/null
+++ b/tests/check_symlinks_test.py
@@ -0,0 +1,16 @@
+import os
+
+import pytest
+
+from pre_commit_hooks.check_symlinks import check_symlinks
+from testing.util import get_resource_path
+
+
+@pytest.mark.xfail(os.name == 'nt', reason='No symlink support on windows')
+@pytest.mark.parametrize(('filename', 'expected_retval'), (
+    ('broken_symlink', 1),
+    ('working_symlink', 0),
+))
+def test_check_symlinks(filename, expected_retval):
+    ret = check_symlinks([get_resource_path(filename)])
+    assert ret == expected_retval
Repository: pre-commit/pre-commit-hooks
Pull request #127 (2016-07-02 22:50:19)

handle editable requirements properly
Requirement fixer does not handles the requirements of the format `-e git+ssh://git_url@tag#egg=req` properly.
This results in these kind of requirements always ordered to the file top.
This is not in sync with the output given by `pip freeze`.



Patch:
diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py
index bf7f75d..64b5e47 100644
--- a/pre_commit_hooks/requirements_txt_fixer.py
+++ b/pre_commit_hooks/requirements_txt_fixer.py
@@ -10,6 +10,13 @@ class Requirement(object):
         self.value = None
         self.comments = []
 
+    @property
+    def name(self):
+        if self.value.startswith(b'-e '):
+            return self.value.lower().partition(b'=')[-1]
+
+        return self.value.lower().partition(b'==')[0]
+
     def __lt__(self, requirement):
         # \n means top of file comment, so always return True,
         # otherwise just do a string comparison with value.
@@ -18,10 +25,7 @@ class Requirement(object):
         elif requirement.value == b'\n':
             return False
         else:
-            return (
-                self.value.lower().partition(b'==') <
-                requirement.value.lower().partition(b'==')
-            )
+            return self.name < requirement.name
 
 
 def fix_requirements(f):


Test patch:
diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py
index fe51f4a..1c590a5 100644
--- a/tests/requirements_txt_fixer_test.py
+++ b/tests/requirements_txt_fixer_test.py
@@ -15,6 +15,7 @@ TESTS = (
     (b'\nbar\nfoo\n', 0, b'\nbar\nfoo\n'),
     (b'pyramid==1\npyramid-foo==2\n', 0, b'pyramid==1\npyramid-foo==2\n'),
     (b'ocflib\nDjango\nPyMySQL\n', 1, b'Django\nocflib\nPyMySQL\n'),
+    (b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', 1, b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n'),
 )
Repository: pre-commit/pre-commit-hooks
Pull request #160 (2016-12-26 18:11:25)

Detect Ed25519 keys as well
`Ed255519` keys generated by OpenSSH contain "`BEGIN OPENSSH PRIVATE KEY`"
as identifier. This commit adds coverage for such keys as well.


Patch:
diff --git a/pre_commit_hooks/detect_private_key.py b/pre_commit_hooks/detect_private_key.py
index 1a4f323..d187364 100644
--- a/pre_commit_hooks/detect_private_key.py
+++ b/pre_commit_hooks/detect_private_key.py
@@ -7,6 +7,7 @@ BLACKLIST = [
     b'BEGIN RSA PRIVATE KEY',
     b'BEGIN DSA PRIVATE KEY',
     b'BEGIN EC PRIVATE KEY',
+    b'BEGIN OPENSSH PRIVATE KEY',
 ]
 
 


Test patch:
diff --git a/tests/detect_private_key_test.py b/tests/detect_private_key_test.py
index 4f2bb93..c6558ba 100644
--- a/tests/detect_private_key_test.py
+++ b/tests/detect_private_key_test.py
@@ -7,6 +7,7 @@ TESTS = (
     (b'-----BEGIN RSA PRIVATE KEY-----', 1),
     (b'-----BEGIN DSA PRIVATE KEY-----', 1),
     (b'-----BEGIN EC PRIVATE KEY-----', 1),
+    (b'-----BEGIN OPENSSH PRIVATE KEY-----', 1),
     (b'ssh-rsa DATA', 0),
     (b'ssh-dsa DATA', 0),
     # Some arbitrary binary data
Repository: pre-commit/pre-commit-hooks
Pull request #230 (2017-08-15 03:21:13)

debug statements hook works for non-utf8 files


Patch:
diff --git a/pre_commit_hooks/debug_statement_hook.py b/pre_commit_hooks/debug_statement_hook.py
index d76e6e6..c5ca387 100644
--- a/pre_commit_hooks/debug_statement_hook.py
+++ b/pre_commit_hooks/debug_statement_hook.py
@@ -35,7 +35,7 @@ class ImportStatementParser(ast.NodeVisitor):
 
 def check_file_for_debug_statements(filename):
     try:
-        ast_obj = ast.parse(open(filename).read(), filename=filename)
+        ast_obj = ast.parse(open(filename, 'rb').read(), filename=filename)
     except SyntaxError:
         print('{} - Could not parse ast'.format(filename))
         print()


Test patch:
diff --git a/tests/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py
index 6d8d7d8..8832245 100644
--- a/tests/debug_statement_hook_test.py
+++ b/tests/debug_statement_hook_test.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
 import ast
 
 import pytest
@@ -77,3 +81,9 @@ def test_returns_zero_for_passing_file():
 def test_syntaxerror_file():
     ret = debug_statement_hook([get_resource_path('cannot_parse_ast.notpy')])
     assert ret == 1
+
+
+def test_non_utf8_file(tmpdir):
+    f_py = tmpdir.join('f.py')
+    f_py.write_binary('# -*- coding: cp1252 -*-\nx = "€"\n'.encode('cp1252'))
+    assert debug_statement_hook((f_py.strpath,)) == 0
Repository: pre-commit/pre-commit-hooks
Pull request #243 (2017-10-09 17:59:54)

Fix requirements-txt-fixer for comments at end of file


Patch:
diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py
index ffabf2a..1ee6fac 100644
--- a/pre_commit_hooks/requirements_txt_fixer.py
+++ b/pre_commit_hooks/requirements_txt_fixer.py
@@ -63,9 +63,16 @@ def fix_requirements(f):
         else:
             requirement.value = line
 
+    # if a file ends in a comment, preserve it at the end
+    if requirements[-1].value is None:
+        rest = requirements.pop().comments
+    else:
+        rest = []
+
     for requirement in sorted(requirements):
         after.extend(requirement.comments)
         after.append(requirement.value)
+    after.extend(rest)
 
     after_string = b''.join(after)
 
diff --git a/pre_commit_hooks/trailing_whitespace_fixer.py b/pre_commit_hooks/trailing_whitespace_fixer.py
index 26f7b51..d44750d 100644
--- a/pre_commit_hooks/trailing_whitespace_fixer.py
+++ b/pre_commit_hooks/trailing_whitespace_fixer.py
@@ -4,6 +4,8 @@ import argparse
 import os
 import sys
 
+from pre_commit_hooks.util import cmd_output
+
 
 def _fix_file(filename, is_markdown):
     with open(filename, mode='rb') as file_processed:
@@ -48,6 +50,10 @@ def fix_trailing_whitespace(argv=None):
     parser.add_argument('filenames', nargs='*', help='Filenames to fix')
     args = parser.parse_args(argv)
 
+    bad_whitespace_files = cmd_output(
+        'grep', '-l', '[[:space:]]$', *args.filenames, retcode=None
+    ).strip().splitlines()
+
     md_args = args.markdown_linebreak_ext
     if '' in md_args:
         parser.error('--markdown-linebreak-ext requires a non-empty argument')
@@ -67,11 +73,11 @@ def fix_trailing_whitespace(argv=None):
             )
 
     return_code = 0
-    for filename in args.filenames:
-        _, extension = os.path.splitext(filename.lower())
+    for bad_whitespace_file in bad_whitespace_files:
+        _, extension = os.path.splitext(bad_whitespace_file.lower())
         md = all_markdown or extension in md_exts
-        if _fix_file(filename, md):
-            print('Fixing {}'.format(filename))
+        if _fix_file(bad_whitespace_file, md):
+            print('Fixing {}'.format(bad_whitespace_file))
             return_code = 1
     return return_code
 


Test patch:
diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py
index dcf7a76..87e7b0c 100644
--- a/tests/requirements_txt_fixer_test.py
+++ b/tests/requirements_txt_fixer_test.py
@@ -11,6 +11,8 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
     (
         (b'', PASS, b''),
         (b'\n', PASS, b'\n'),
+        (b'# intentionally empty\n', PASS, b'# intentionally empty\n'),
+        (b'foo\n# comment at end\n', PASS, b'foo\n# comment at end\n'),
         (b'foo\nbar\n', FAIL, b'bar\nfoo\n'),
         (b'bar\nfoo\n', PASS, b'bar\nfoo\n'),
         (b'#comment1\nfoo\n#comment2\nbar\n', FAIL, b'#comment2\nbar\n#comment1\nfoo\n'),