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
12
13
14def install_git_hook() -> None:
15 hook_path = os.path.join(".git", "hooks", "pre-commit")
16 if os.path.exists(hook_path):
17 print("pre-commit hook already exists")
18 else:
19 with open(hook_path, "w") as f:
20 f.write(
21 """#!/bin/sh
22plain pre-commit"""
23 )
24 os.chmod(hook_path, 0o755)
25 print("pre-commit hook installed")
26
27
28@register_cli("pre-commit")
29@click.command()
30@click.option("--install", is_flag=True)
31def cli(install: bool) -> None:
32 """Git pre-commit checks"""
33 if install:
34 install_git_hook()
35 return
36
37 pyproject = Path("pyproject.toml")
38
39 if pyproject.exists():
40 with open(pyproject, "rb") as f:
41 pyproject = tomllib.load(f)
42 for name, data in (
43 pyproject.get("tool", {})
44 .get("plain", {})
45 .get("pre-commit", {})
46 .get("run", {})
47 ).items():
48 cmd = data["cmd"]
49 print_event(f"Custom: {name} -> {cmd}")
50 result = subprocess.run(cmd, shell=True)
51 if result.returncode != 0:
52 sys.exit(result.returncode)
53
54 # Run this first since it's probably the most likely to fail
55 if find_spec("plain.code"):
56 check_short("Running plain code checks", "plain", "code", "check")
57
58 if Path("uv.lock").exists():
59 check_short("Checking uv.lock", "uv", "lock", "--check")
60
61 if plain_db_connected():
62 check_short(
63 "Running preflight checks",
64 "plain",
65 "preflight",
66 "check",
67 "--quiet",
68 )
69 check_short("Checking Plain migrations", "plain", "migrate", "--check")
70 check_short(
71 "Checking for Plain models missing migrations",
72 "plain",
73 "makemigrations",
74 "--dry-run",
75 "--check",
76 )
77 else:
78 check_short(
79 "Running Plain checks (without database)",
80 "plain",
81 "preflight",
82 "check",
83 "--quiet",
84 )
85 click.secho("--> Skipping migration checks", bold=True, fg="yellow")
86
87 print_event("Running plain build")
88 result = subprocess.run(["plain", "build"])
89 if result.returncode != 0:
90 sys.exit(result.returncode)
91
92 if find_spec("plain.pytest"):
93 print_event("Running tests")
94 result = subprocess.run(["plain", "test"])
95 if result.returncode != 0:
96 sys.exit(result.returncode)
97
98
99def plain_db_connected() -> bool:
100 result = subprocess.run(
101 [
102 "plain",
103 "models",
104 "show-migrations",
105 ],
106 stdout=subprocess.DEVNULL,
107 stderr=subprocess.DEVNULL,
108 )
109 return result.returncode == 0
110
111
112def check_short(message: str, *args: str) -> None:
113 print_event(message, newline=False)
114 result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
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")