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(
50 click.style(f"Custom[{name}]:", bold=True)
51 + click.style(f" {cmd}", dim=True),
52 newline=False,
53 )
54 result = subprocess.run(cmd, shell=True)
55 if result.returncode != 0:
56 sys.exit(result.returncode)
57
58 # Run this first since it's probably the most likely to fail
59 if find_spec("plain.code"):
60 check_short(
61 click.style("Code:", bold=True)
62 + click.style(" plain code check", dim=True),
63 "plain",
64 "code",
65 "check",
66 )
67
68 if Path("uv.lock").exists():
69 check_short(
70 click.style("Dependencies:", bold=True)
71 + click.style(" uv lock --check", dim=True),
72 "uv",
73 "lock",
74 "--check",
75 )
76
77 if plain_db_connected():
78 check_short(
79 click.style("Preflight:", bold=True)
80 + click.style(" plain preflight", dim=True),
81 "plain",
82 "preflight",
83 "--quiet",
84 )
85 check_short(
86 click.style("Migrate:", bold=True)
87 + click.style(" plain migrate --check", dim=True),
88 "plain",
89 "migrate",
90 "--check",
91 )
92 check_short(
93 click.style("Migrations:", bold=True)
94 + click.style(" plain makemigrations --dry-run --check", dim=True),
95 "plain",
96 "makemigrations",
97 "--dry-run",
98 "--check",
99 )
100 else:
101 check_short(
102 click.style("Preflight:", bold=True)
103 + click.style(" plain preflight", dim=True),
104 "plain",
105 "preflight",
106 "--quiet",
107 )
108 click.secho("--> Skipping migration checks", bold=True, fg="yellow")
109
110 check_short(
111 click.style("Build:", bold=True) + click.style(" plain build", dim=True),
112 "plain",
113 "build",
114 )
115
116 if find_spec("plain.pytest"):
117 print_event(
118 click.style("Test:", bold=True) + click.style(" plain test", dim=True)
119 )
120 result = subprocess.run(["plain", "test"])
121 if result.returncode != 0:
122 sys.exit(result.returncode)
123
124
125def plain_db_connected() -> bool:
126 result = subprocess.run(
127 [
128 "plain",
129 "migrations",
130 "list",
131 ],
132 stdout=subprocess.DEVNULL,
133 stderr=subprocess.DEVNULL,
134 )
135 return result.returncode == 0
136
137
138def check_short(message: str, *args: str) -> None:
139 print_event(message, newline=False)
140 result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
141 if result.returncode != 0:
142 click.secho("โ", fg="red")
143 click.secho(result.stdout.decode("utf-8"))
144 sys.exit(1)
145 else:
146 click.secho("โ", fg="green")