diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index e7f433b..c681c45 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -105,6 +105,12 @@ entry: end-of-file-fixer language: python files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ +- id: file-contents-sorter + name: File Contents Sorter + description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. + entry: file-contents-sorter + language: python + files: '^$' - id: fix-encoding-pragma name: Fix python encoding pragma language: python diff --git a/README.md b/README.md index 8db7eef..92fb408 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Add this to your `.pre-commit-config.yaml` with single quoted strings. - `end-of-file-fixer` - Makes sure files end in a newline and only a newline. - `fix-encoding-pragma` - Add `# -*- coding: utf-8 -*-` to the top of python files. +- `file-contents-sorter` - Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input to it. - To remove the coding pragma pass `--remove` (useful in a python3-only codebase) - `flake8` - Run flake8 on your python files. - `forbid-new-submodules` - Prevent addition of new git submodules. diff --git a/hooks.yaml b/hooks.yaml index e7f433b..c681c45 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -105,6 +105,12 @@ entry: end-of-file-fixer language: python files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ +- id: file-contents-sorter + name: File Contents Sorter + description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. + entry: file-contents-sorter + language: python + files: '^$' - id: fix-encoding-pragma name: Fix python encoding pragma language: python diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py new file mode 100644 index 0000000..6fa3bc0 --- /dev/null +++ b/pre_commit_hooks/file_contents_sorter.py @@ -0,0 +1,52 @@ +""" +A very simple pre-commit hook that, when passed one or more filenames +as arguments, will sort the lines in those files. + +An example use case for this: you have a deploy-whitelist.txt file +in a repo that contains a list of filenames that is used to specify +files to be included in a docker container. This file has one filename +per line. Various users are adding/removing lines from this file; using +this hook on that file should reduce the instances of git merge +conflicts and keep the file nicely ordered. +""" +from __future__ import print_function + +import argparse + +PASS = 0 +FAIL = 1 + + +def sort_file_contents(f): + before = tuple(f) + after = sorted(before) + + before_string = b''.join(before) + after_string = b''.join(after) + + if before_string == after_string: + return PASS + else: + f.seek(0) + f.write(after_string) + f.truncate() + return FAIL + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='+', help='Files to sort') + args = parser.parse_args(argv) + + retv = PASS + + for arg in args.filenames: + with open(arg, 'rb+') as file_obj: + ret_for_file = sort_file_contents(file_obj) + + if ret_for_file: + print('Sorting {}'.format(arg)) + + retv |= ret_for_file + + return retv diff --git a/setup.py b/setup.py index af21e16..c5cceb7 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ setup( 'detect-private-key = pre_commit_hooks.detect_private_key:detect_private_key', 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', + 'file-contents-sorter = pre_commit_hooks.file_contents_sorter:main', 'fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main', 'forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py new file mode 100644 index 0000000..5f4dc5b --- /dev/null +++ b/tests/file_contents_sorter_test.py @@ -0,0 +1,29 @@ +import pytest + +from pre_commit_hooks.file_contents_sorter import FAIL +from pre_commit_hooks.file_contents_sorter import main +from pre_commit_hooks.file_contents_sorter import PASS + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + ( + (b'', PASS, b''), + (b'lonesome\n', PASS, b'lonesome\n'), + (b'missing_newline', PASS, b'missing_newline'), + (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'), + (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'), + (b'C\nc\n', PASS, b'C\nc\n'), + (b'c\nC\n', FAIL, b'C\nc\n'), + (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'), + (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'), + ) +) +def test_integration(input_s, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) + + output_retval = main([path.strpath]) + + assert path.read_binary() == output + assert output_retval == expected_retval