Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
pre-commit 3.27 KiB
#!/usr/bin/env python3

"""
    This pre-commit hook checks each staged file if it complies to the
    formatting rules, as defined by clang-format (or the programm specified
    in `git config hooks.clangformat`).
"""

import os
import subprocess
import sys
import tempfile

CPP_EXTENSIONS = ('.c', '.cc', '.cxx', '.h', '.hh', '.hxx', '.pbh')


class FormatRunException(RuntimeError):
    pass


class NonEmptyDiffException(Exception):
    pass


def check_dir(basedir, mode='staged'):
    """
    :param basedir: absolute path to git root
    :param mode: anyof 'staged', 'modified' or 'all' to select which files to check. Note
                    staged files do not count as modified
    :return: list of filename with non-empty diff to current state after applying clang-format
    """
    commands = {'staged': ['git', 'diff', '--name-only', '--cached'],
                'modified': ['git', 'ls-files', '-m'],
                'all': ['git', 'ls-files']}
    os.chdir(basedir)
    # alternative clang-format binary given?
    try:
        clangformat = subprocess.check_output(['git', 'config', 'hooks.clangformat'],
                                              universal_newlines=True).strip()
    except subprocess.CalledProcessError as _:
        clangformat = 'clang-format'

    out = subprocess.check_output(commands[mode],
                                  universal_newlines=True)
    fails = []
    for changed_file in sorted(out.splitlines()):
        try:
            check_file(changed_file, basedir, clangformat)
        except NonEmptyDiffException:
            fails.append(changed_file)
    return fails


def check_file(touched_file, basedir, format_binary, extension_whitelist=CPP_EXTENSIONS):
    if os.path.splitext(touched_file)[1] not in extension_whitelist:
        return
    filename = os.path.join(basedir, touched_file)
    if not os.path.isfile(filename):
        return

    try:
        out = subprocess.check_output([format_binary, '-style=file', filename],
                                    universal_newlines=True)
    except subprocess.CalledProcessError as e:
        raise FormatRunException('Error executing {} on file\n{}'.format(format_binary, filename, e.output))
    tmp_filename = tempfile.NamedTemporaryFile(mode='wb', delete=False).name
    with open(tmp_filename, 'w') as tmp_file:
        tmp_file.write(out)
    try:
        subprocess.check_output(['diff', filename, tmp_filename],
                                      universal_newlines=True)
    except subprocess.CalledProcessError as res:
        if res.returncode == 1:
            raise NonEmptyDiffException('clang-format has to be applied to \'{}\'!'.format(filename))
        raise RuntimeError('error diffing {} vs {}'.format(touched_file, tmp_filename))
    finally:
        os.remove(tmp_filename)

if __name__ == '__main__':
    dirname = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.getcwd())
    try:
        fails = check_dir(dirname, mode='staged')
    except FileNotFoundError as err:
        raise RuntimeError("""If you have trouble with clang-format, you can specify which version to use with

    git config hooks.clangformat clang-format

The original error was: {}""".format(err))
    if len(fails) > 0:
        print('files need to be formatted:\n\t{}'.format('\n\t'.join(fails)) )
    sys.exit(len(fails))