Skip to content
Snippets Groups Projects
Verified Commit 84897182 authored by René Fritze's avatar René Fritze
Browse files

[hooks] adds multihook infrastructure

you can now chain multiple hook-scripts per
hook name
see hooks/multihooks.py docstring for detailed instructions
parent 35b5b611
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2015-2017 Carlos Jenkins <carlos@jenkins.co.cr>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Small delegating Python script to allow multiple hooks in a git repository.
Usage:
Make your building system to create a symbolic link in the git hooks directory
to this script with the name of the hook you want to attend. For example,
``pre-commit``.
This hook will then execute, in alphabetic order, all executables files
(subhooks) found under a folder named after the hook type you're attending
suffixed with ``.d``. For example, ``pre-commit.d``.
For example:
```
.git/hooks/
|_ pre-commit
|_ pre-commit.d/
|_ 01-cpp_coding_standard
|_ 02-python_coding_standard
|_ 03-something_else
```
Additionally, colored logging is supported if the package colorlog is
installed.
"""
from sys import argv
from logging import getLogger
from subprocess import Popen, PIPE
from os import access, listdir, X_OK, environ
from os.path import isfile, isdir, abspath, normpath, dirname, join, basename
GIT_HOOKS = [
'applypatch-msg',
'commit-msg',
'post-update',
'pre-applypatch',
'pre-commit',
'prepare-commit-msg',
'pre-push',
'pre-rebase',
'update',
]
def setup_logging():
"""
Setup logging with support for colored output if available.
"""
from logging import basicConfig, DEBUG, ERROR
FORMAT = (
' %(log_color)s%(levelname)-8s%(reset)s | '
'%(log_color)s%(message)s%(reset)s'
)
logging_kwargs = {
'level': DEBUG if environ.get('GIT_HOOKS_DEBUG', False) else ERROR,
}
try:
from logging import StreamHandler
from colorlog import ColoredFormatter
stream = StreamHandler()
stream.setFormatter(ColoredFormatter(FORMAT))
logging_kwargs['handlers'] = [stream]
except ImportError:
pass
basicConfig(**logging_kwargs)
def main():
"""
Execute subhooks for the assigned hook type.
"""
setup_logging()
log = getLogger(basename(__file__))
# Check multihooks facing what hook type
hook_type = basename(__file__)
if hook_type not in GIT_HOOKS:
log.fatal('Unknown hook type: {}'.format(hook_type))
exit(1)
# Lookup for sub-hooks directory
root = normpath(abspath(dirname(__file__)))
hooks_dir = join(root, '{}.d'.format(hook_type))
if not isdir(hooks_dir):
log.warning('No such directory: {}'.format(hooks_dir))
exit(0)
# Gather scripts to call
files = [join(hooks_dir, f) for f in listdir(hooks_dir)]
hooks = sorted(
[h for h in files if isfile(h) and access(h, X_OK)]
)
if not hooks:
log.warning('No sub-hooks found for {}.'.format(hook_type))
exit(0)
# Execute hooks
for h in hooks:
hook_id = '{}.d/{}'.format(hook_type, basename(h))
log.info('Running hook {}...'.format(hook_id))
proc = Popen([h] + argv[1:], stdout=PIPE, stderr=PIPE)
stdout_raw, stderr_raw = proc.communicate()
stdout = stdout_raw.decode('utf-8').strip()
stderr = stderr_raw.decode('utf-8').strip()
if stdout:
log.info(stdout)
if stderr:
log.error(stderr)
# Log errors if a hook failed
if proc.returncode != 0:
log.error('Hook {} failed. Aborting...'.format(hook_id))
exit(proc.returncode)
if __name__ == '__main__':
main()
#!/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))
./multihooks.py
\ No newline at end of file
#!/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))
......@@ -6,4 +6,4 @@ SRC_HOOKDIR="$(cd "$(dirname ${BASH_SOURCE[0]})/../hooks" ; pwd -P )"
# this _might_ fail with git >= 2.9 if core.hooksPath is set
DST_HOOKDIR="$(git rev-parse --git-dir)/hooks"
cp -a ${SRC_HOOKDIR}/* ${DST_HOOKDIR}
cp -ra ${SRC_HOOKDIR}/* ${DST_HOOKDIR}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment