mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-02-23 19:50:06 -05:00
Merge branch 'git-hooks' of https://github.com/un-pogaz/calibre
This commit is contained in:
commit
d2009d036c
@ -47,6 +47,10 @@ class Check(Command):
|
||||
def add_options(self, parser):
|
||||
parser.add_option('--fix', '--auto-fix', default=False, action='store_true',
|
||||
help='Try to automatically fix some of the smallest errors instead of opening an editor for bad files.')
|
||||
parser.add_option('-f', '--file', dest='files', type='string', action='append',
|
||||
help='Specific file to be check. Can be repeat to check severals.')
|
||||
parser.add_option('--no-editor', default=False, action='store_true',
|
||||
help="Don't open the editor when a bad file is found.")
|
||||
|
||||
def get_files(self):
|
||||
yield from checkable_python_files(self.SRC)
|
||||
@ -83,9 +87,9 @@ class Check(Command):
|
||||
ext = os.path.splitext(f)[1]
|
||||
if ext in {'.py', '.recipe'}:
|
||||
if self.auto_fix:
|
||||
p = subprocess.Popen(['ruff', 'check', '--fix', f])
|
||||
p = subprocess.Popen(['ruff', 'check', '-q', '--fix', f])
|
||||
else:
|
||||
p = subprocess.Popen(['ruff', 'check', f])
|
||||
p = subprocess.Popen(['ruff', 'check', '-q', f])
|
||||
return p.wait() != 0
|
||||
if ext == '.pyj':
|
||||
p = subprocess.Popen(['rapydscript', 'lint', f])
|
||||
@ -99,6 +103,8 @@ class Check(Command):
|
||||
self.wn_path = os.path.expanduser('~/work/srv/main/static')
|
||||
self.has_changelog_check = os.path.exists(self.wn_path)
|
||||
self.auto_fix = opts.fix
|
||||
self.files = opts.files
|
||||
self.no_editor = opts.no_editor
|
||||
self.run_check_files()
|
||||
|
||||
def run_check_files(self):
|
||||
@ -109,18 +115,24 @@ class Check(Command):
|
||||
except OSError as err:
|
||||
if err.errno != errno.ENOENT:
|
||||
raise
|
||||
dirty_files = tuple(f for f in self.get_files() if not self.is_cache_valid(f, cache))
|
||||
if self.files:
|
||||
dirty_files = tuple(self.files)
|
||||
else:
|
||||
dirty_files = tuple(f for f in self.get_files() if not self.is_cache_valid(f, cache))
|
||||
try:
|
||||
for i, f in enumerate(dirty_files):
|
||||
self.info('\tChecking', f)
|
||||
if self.file_has_errors(f):
|
||||
self.info(f'{len(dirty_files) - i - 1} files left to check')
|
||||
e = SystemExit(1)
|
||||
if self.no_editor:
|
||||
raise e
|
||||
try:
|
||||
edit_file(f)
|
||||
except FileNotFoundError:
|
||||
pass # continue if the configured editor fail to be open
|
||||
raise e # raise immediately to skip second check
|
||||
if self.file_has_errors(f):
|
||||
raise SystemExit(1)
|
||||
raise e
|
||||
cache[f] = self.file_hash(f)
|
||||
finally:
|
||||
self.save_cache(cache)
|
||||
|
||||
@ -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()
|
||||
|
||||
126
setup/git_hooks.py
Normal file
126
setup/git_hooks.py
Normal file
@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, un_pogaz <un.pogaz@gmail.com>
|
||||
|
||||
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),
|
||||
Hook('pre-commit', 'git_pre_commit_hook.py', 0, 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)
|
||||
@ -21,7 +21,7 @@ if flags == '1': # A branch checkout
|
||||
prev_branch, cur_branch = list(map(get_branch_name, (prev_rev, current_rev)))
|
||||
rebase_in_progress = os.path.exists('.git/rebase-apply') or os.path.exists('.git/rebase-merge')
|
||||
|
||||
subprocess.check_call('./setup.py gui --summary'.split())
|
||||
subprocess.check_call([sys.executable, './setup.py', 'gui', '--summary'])
|
||||
|
||||
# Remove .pyc files as some of them might have been orphaned
|
||||
for dirpath, dirnames, filenames in os.walk('.'):
|
||||
|
||||
@ -13,4 +13,4 @@ os.chdir(base)
|
||||
action = [x.decode('utf-8') if isinstance(x, bytes) else x for x in sys.argv[1:]][0]
|
||||
|
||||
if action == 'rebase':
|
||||
subprocess.check_call(['./setup.py', 'gui', '--summary'])
|
||||
subprocess.check_call([sys.executable, './setup.py', 'gui', '--summary'])
|
||||
|
||||
47
setup/git_pre_commit_hook.py
Normal file
47
setup/git_pre_commit_hook.py
Normal file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
base = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
os.chdir(base)
|
||||
setup_py = os.path.realpath('./setup.py')
|
||||
|
||||
|
||||
def testfile(file):
|
||||
def t(f, start, end, exclude_end=None):
|
||||
return f.startswith(start) and f.endswith(end) and not f.endswith(exclude_end) if exclude_end else True
|
||||
if t(file, ('src/odf', 'src/calibre'), '.py', exclude_end='_ui.py'):
|
||||
return True
|
||||
if t(file, 'recipes', '.recipe'):
|
||||
return True
|
||||
if t(file, 'src/pyj', '.pyj'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
output = subprocess.check_output((
|
||||
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
|
||||
# Everything except for D
|
||||
'--diff-filter=ACMRTUXB',
|
||||
)).decode('utf-8')
|
||||
|
||||
output = output.strip('\0')
|
||||
if not output:
|
||||
output = []
|
||||
else:
|
||||
output = output.split('\0')
|
||||
|
||||
filenames = tuple(filter(testfile, output))
|
||||
if not filenames:
|
||||
sys.exit(0)
|
||||
|
||||
check_args = [sys.executable, './setup.py', 'check', '--no-editor']
|
||||
# let's hope that too many arguments do not hold any surprises
|
||||
for f in filenames:
|
||||
check_args.append('-f')
|
||||
check_args.append(f)
|
||||
|
||||
returncode = subprocess.call(check_args)
|
||||
sys.exit(returncode)
|
||||
Loading…
x
Reference in New Issue
Block a user