Skip to content

Instantly share code, notes, and snippets.

@carlos-jenkins
Last active August 10, 2023 22:12
Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save carlos-jenkins/89da9dcf9e0d528ac978311938aade43 to your computer and use it in GitHub Desktop.
Save carlos-jenkins/89da9dcf9e0d528ac978311938aade43 to your computer and use it in GitHub Desktop.
Delegating script for multiple git hooks
#!/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
from os.path import isfile, isdir, abspath, normpath, dirname, join, basename
GIT_HOOKS = [
'applypatch-msg',
'commit-msg',
'fsmonitor-watchman',
'post-checkout',
'post-commit',
'post-merge',
'post-update',
'pre-applypatch',
'pre-commit',
'pre-push',
'pre-rebase',
'pre-receive',
'prepare-commit-msg',
'update',
]
def setup_logging():
"""
Setup logging with support for colored output if available.
"""
from logging import basicConfig, DEBUG
FORMAT = (
' %(log_color)s%(levelname)-8s%(reset)s | '
'%(log_color)s%(message)s%(reset)s'
)
logging_kwargs = {
'level': DEBUG,
}
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()
@isaiah1112
Copy link

This script is awesome! Thanks for putting it together!

@mpoullet
Copy link

Excellent!

I've changed
log = getLogger('multihooks')
to
log = getLogger(basename(__file__))
though.

@carlos-jenkins
Copy link
Author

Hi all!

I updated the script to pass the arguments received to the subhooks (so that for example commit-msg works) and also added support for optional colored logging.

Regards

@revolter
Copy link

revolter commented Oct 5, 2017

Why isn't this supporting post- hooks?

@carlos-jenkins
Copy link
Author

@Revolver when I created the list of GIT_HOOKS I just:

ls -lsh .git/hooks/

It is just a protection against typos. You're free to add the ones missing.

@damienrg
Copy link

damienrg commented Nov 8, 2018

Bash version inspired from your script but with stdin copied. This permits to have multiple scripts which read data from stdin.

@tomasz-wiszkowski
Copy link

tomasz-wiszkowski commented Dec 28, 2018

This is great! Can you, please, also support post-commit hooks?

@carlos-jenkins
Copy link
Author

@tomasz-wiszkowski I think I got them all now.

@guptarohit
Copy link

I wrote about how to automate code formatting tasks with the pre-commit framework.
Using pre-commit hooks to keep python code clean with black, flake8 and isort. ✌️
☮️ 🍰 ✨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment