#!/usr/bin/env python3 # Copyright (c) 2009 Giampaolo Rodola'. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """This gets executed on 'git commit' and rejects the commit in case the submitted code does not pass validation. Validation is run only against the files which were modified in the commit. Install this with "make install-git-hooks". """ import os import shlex import subprocess import sys PYTHON = sys.executable def term_supports_colors(): try: import curses assert sys.stderr.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 except Exception: # noqa: BLE001 return False return True def hilite(s, ok=True, bold=False): """Return an highlighted version of 'string'.""" if not term_supports_colors(): return s attr = [] if ok is None: # no color pass elif ok: # green attr.append('32') else: # red attr.append('31') if bold: attr.append('1') return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" def exit(msg): print(hilite("commit aborted: " + msg, ok=False), file=sys.stderr) sys.exit(1) def sh(cmd): if isinstance(cmd, str): cmd = shlex.split(cmd) p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) if stderr: print(stderr, file=sys.stderr) if stdout.endswith('\n'): stdout = stdout[:-1] return stdout def git_commit_files(): out = sh(["git", "diff", "--cached", "--name-only"]) py_files = [ x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x) ] c_files = [ x for x in out.split('\n') if x.endswith(('.c', '.h')) and os.path.exists(x) ] rst_files = [ x for x in out.split('\n') if x.endswith('.rst') and os.path.exists(x) ] toml_files = [ x for x in out.split("\n") if x.endswith(".toml") and os.path.exists(x) ] new_rm_mv = sh( ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] ) # XXX: we should escape spaces and possibly other amenities here new_rm_mv = new_rm_mv.split() return (py_files, c_files, rst_files, toml_files, new_rm_mv) def black(files): print(f"running black ({len(files)})") cmd = [PYTHON, "-m", "black", "--check", "--safe"] + files if subprocess.call(cmd) != 0: return exit( "Python code didn't pass 'ruff' style check." "Try running 'make fix-ruff'." ) def ruff(files): print(f"running ruff ({len(files)})") cmd = [ PYTHON, "-m", "ruff", "check", "--no-cache", "--output-format=concise", ] + files if subprocess.call(cmd) != 0: return exit( "Python code didn't pass 'ruff' style check." "Try running 'make fix-ruff'." ) def c_linter(files): print(f"running clinter ({len(files)})") # XXX: we should escape spaces and possibly other amenities here cmd = [PYTHON, "scripts/internal/clinter.py"] + files if subprocess.call(cmd) != 0: return sys.exit("C code didn't pass style check") def toml_sort(files): print(f"running toml linter ({len(files)})") cmd = ["toml-sort", "--check"] + files if subprocess.call(cmd) != 0: return sys.exit(f"{' '.join(files)} didn't pass style check") def rstcheck(files): print(f"running rst linter ({len(files)})") cmd = ["rstcheck", "--config=pyproject.toml"] + files if subprocess.call(cmd) != 0: return sys.exit("RST code didn't pass style check") def main(): py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() if py_files: black(py_files) ruff(py_files) if c_files: c_linter(c_files) if rst_files: rstcheck(rst_files) if toml_files: toml_sort(toml_files) if new_rm_mv: out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open("MANIFEST.in", encoding="utf8") as f: if out.strip() != f.read().strip(): sys.exit( "some files were added, deleted or renamed; " "run 'make generate-manifest' and commit again" ) if __name__ == "__main__": main()