#!/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)