Plain is headed towards 1.0! Subscribe for development updates โ†’

  1import os
  2import subprocess
  3import sys
  4import tomllib
  5from importlib.util import find_spec
  6from pathlib import Path
  7
  8import click
  9
 10from plain.cli import register_cli
 11from plain.cli.print import print_event
 12from plain.cli.runtime import without_runtime_setup
 13
 14
 15def install_git_hook() -> None:
 16    hook_path = os.path.join(".git", "hooks", "pre-commit")
 17    if os.path.exists(hook_path):
 18        print("pre-commit hook already exists")
 19    else:
 20        with open(hook_path, "w") as f:
 21            f.write(
 22                """#!/bin/sh
 23plain pre-commit"""
 24            )
 25        os.chmod(hook_path, 0o755)
 26        print("pre-commit hook installed")
 27
 28
 29@register_cli("pre-commit")
 30@click.group(invoke_without_command=True)
 31@click.pass_context
 32def cli(ctx: click.Context) -> None:
 33    """Git pre-commit checks"""
 34    # If no subcommand is provided, run the checks
 35    if ctx.invoked_subcommand is None:
 36        run_checks()
 37
 38
 39@cli.command()
 40@without_runtime_setup
 41def install() -> None:
 42    """Install the pre-commit git hook"""
 43    install_git_hook()
 44
 45
 46def run_checks() -> None:
 47    """Run all pre-commit checks"""
 48
 49    pyproject = Path("pyproject.toml")
 50
 51    if pyproject.exists():
 52        with open(pyproject, "rb") as f:
 53            pyproject = tomllib.load(f)
 54        for name, data in (
 55            pyproject.get("tool", {})
 56            .get("plain", {})
 57            .get("pre-commit", {})
 58            .get("run", {})
 59        ).items():
 60            cmd = data["cmd"]
 61            print_event(f"Custom: {cmd}")
 62            result = subprocess.run(cmd, shell=True)
 63            if result.returncode != 0:
 64                sys.exit(result.returncode)
 65
 66    # Run this first since it's probably the most likely to fail
 67    if find_spec("plain.code"):
 68        check_short("plain code check", "plain", "code", "check")
 69
 70    if Path("uv.lock").exists():
 71        check_short("uv lock --check", "uv", "lock", "--check")
 72
 73    if plain_db_connected():
 74        check_short("plain preflight", "plain", "preflight", "--quiet")
 75        check_short("plain migrate --check", "plain", "migrate", "--check")
 76        check_short(
 77            "plain makemigrations --dry-run --check",
 78            "plain",
 79            "makemigrations",
 80            "--dry-run",
 81            "--check",
 82        )
 83    else:
 84        check_short("plain preflight", "plain", "preflight", "--quiet")
 85        click.secho("--> Skipping migration checks", bold=True, fg="yellow")
 86
 87    check_short("plain build", "plain", "build")
 88
 89    if find_spec("plain.pytest"):
 90        print_event("plain test")
 91        result = subprocess.run(["plain", "test"])
 92        if result.returncode != 0:
 93            sys.exit(result.returncode)
 94
 95
 96def plain_db_connected() -> bool:
 97    result = subprocess.run(
 98        [
 99            "plain",
100            "migrations",
101            "list",
102        ],
103        stdout=subprocess.DEVNULL,
104        stderr=subprocess.DEVNULL,
105    )
106    return result.returncode == 0
107
108
109def check_short(message: str, *args: str) -> None:
110    print_event(message, newline=False)
111    env = {**os.environ, "FORCE_COLOR": "1"}
112    result = subprocess.run(
113        args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
114    )
115    if result.returncode != 0:
116        click.secho("โœ˜", fg="red")
117        click.secho(result.stdout.decode("utf-8"))
118        sys.exit(1)
119    else:
120        click.secho("โœ”", fg="green")