From e7e1f861837b357c26850b815f3a1b8e98d5ab1a Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:11:41 +0200 Subject: [PATCH] setup.py git_hooks Install/uninstall git hooks --- setup/commands.py | 5 ++ setup/git_hooks.py | 125 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 setup/git_hooks.py diff --git a/setup/commands.py b/setup/commands.py index fa03ad7a9e..faba5b437a 100644 --- a/setup/commands.py +++ b/setup/commands.py @@ -15,6 +15,7 @@ __all__ = [ 'export_packages', 'extdev', 'get_translations', + 'git_hooks', 'git_version', 'gui', 'hyphenation', @@ -102,6 +103,10 @@ from setup.liberation import LiberationFonts liberation_fonts = LiberationFonts() +from setup.git_hooks import GitHooks + +git_hooks = GitHooks() + from setup.git_version import GitVersion git_version = GitVersion() diff --git a/setup/git_hooks.py b/setup/git_hooks.py new file mode 100644 index 0000000000..750474e573 --- /dev/null +++ b/setup/git_hooks.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, un_pogaz + +import os +from collections import namedtuple + +from setup import Command + +base = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +base_hooks = os.path.join(base, '.git', 'hooks') +src_script = os.path.splitext(os.path.realpath(__file__))[0] + + +HOOK_TEMPLATE = '''\ +#!/bin/sh +#!/usr/bin/env bash +# File generated by calibre "setup.py git_hooks" + +HOOK_DIR=$(cd "$(dirname "$0")" && pwd) +BASE_DIR=$(dirname "$(dirname "$HOOK_DIR")") +SCRIPT=$BASE_DIR/{file} + +exec python "$SCRIPT" {args} +''' + +Hook = namedtuple('Hook', ['name', 'file', 'args_count', 'default']) + +HOOKS = {h.name:h for h in ( + Hook('post-checkout', 'git_post_checkout_hook.py', 3, True), + Hook('post-rewrite', 'git_post_rewrite_hook.py', 1, True), + # disable by default, because except Kovid, nobody can run this hook + Hook('commit-msg', 'git_commit_msg_hook.py', 1, False), +)} + +DEFAULT = ','.join(sorted(h.name for h in HOOKS.values() if h.default)) +AVAILABLES = ', '.join(sorted(h for h in HOOKS)) + + +class GitHooks(Command): + description = 'Install/uninstall git hooks' + + def add_options(self, parser): + parser.add_option('-n', '--name', default=DEFAULT, + help='Name(s) of the hook to install, separated by commas. ' + f'Default: "{DEFAULT}". Hooks available: {AVAILABLES}') + parser.add_option('-u', '--uninstall', default=False, action='store_true', + help='Uninstall the selected hooks') + parser.add_option('-f', '--force', default=False, action='store_true', + help='Force the operations on the hooks') + + def run(self, opts): + self.force = opts.force + self.names = [] + + invalides = [] + for candidate in sorted(c.strip().lower() for c in opts.name.split(',')): + if not candidate: + continue + if candidate not in HOOKS: + invalides.append(candidate) + else: + self.names.append(candidate) + + if invalides: + self.info('Info: The following hook names are not recognized:', ', '.join(invalides)) + if not self.names: + self.info('No supported hook names recognized.') + return + + if opts.uninstall: + self.uninstall() + else: + self.install() + + def _parse_template(self, hook_name): + hook = HOOKS[hook_name] + path = self.j(base_hooks, hook.name) + sh_file = f'setup/{hook.file}' + sh_args = ' '.join(f'"${i}"' for i in range(1, hook.args_count+1)) + script = HOOK_TEMPLATE.format(file=sh_file, args=sh_args) + return path, script + + def install(self): + self.info('Installing the hooks:', ', '.join(self.names)) + for candidate in self.names: + path, script = self._parse_template(candidate) + + if self.e(path): + with open(path, 'rb') as f: + previous = f.read().decode('utf-8') + msg = f'{candidate}: a non-calibre hook is installed.' + if previous == script: + self.info(f'{candidate}: installed.') + continue + elif self.force: + self.info(msg, 'Force installation.') + else: + self.info(msg, 'Skip installation.') + continue + + self.info(f'{candidate}: installed.') + with open(path, 'wb') as f: + f.write(script.encode('utf-8')) + + def uninstall(self): + self.info('Uninstalling the hooks:', ', '.join(self.names)) + for candidate in self.names: + path, script = self._parse_template(candidate) + + if not self.e(path): + self.info(f'{candidate}: no hook to unistall.') + continue + + with open(path, 'rb') as f: + previous = f.read().decode('utf-8') + msg = f'{candidate}: a non-calibre hook is installed.' + if previous == script: + self.info(f'{candidate}: unistalled.') + elif self.force: + self.info(msg, 'Force unistallation.') + else: + self.info(msg, 'Skip unistallation.') + continue + + os.remove(path)