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