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")