Skip to content

Commit 8dde05f

Browse files
committed
feat: add git hooks
1 parent 29b3890 commit 8dde05f

File tree

5 files changed

+139
-1
lines changed

5 files changed

+139
-1
lines changed

.githooks/commit-msg

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/sh
2+
3+
set -ex
4+
5+
# Get the git project root directory
6+
project_root=$(git rev-parse --show-toplevel)
7+
8+
# Define the path to the validate script
9+
validate_script="$project_root/bin/validate"
10+
11+
# Check if the script exists
12+
if [ ! -f "$validate_script" ]; then
13+
echo "Error: Validation script not found at: $validate_script" >&2
14+
exit 1
15+
fi
16+
17+
# Check if the script is executable
18+
if [ ! -x "$validate_script" ]; then
19+
echo "Error: Validation script is not executable: $validate_script" >&2
20+
echo "Try running: chmod +x $validate_script" >&2
21+
exit 1
22+
fi
23+
24+
# Execute the validate script from the bin directory
25+
exec "$validate_script" "$1"

.githooks/pre-commit

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
3+
set -ex
4+
5+
composer run fix
6+
npm run fix
7+
8+
if [ -n "$(git diff --name-only)" ]; then
9+
git add .
10+
fi

bin/validate

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/bin/bash
2+
3+
# Function to print error message and exit with failure status
4+
print_error() {
5+
echo "Error: $1" >&2
6+
exit 1
7+
}
8+
9+
# Function to validate the commit message
10+
validate_commit_message() {
11+
local message="$(head -n1 $1)"
12+
13+
echo "$message"
14+
15+
# Regular expressions for different parts of the commit message
16+
local type_regex="^(feat|deps|fix|docs|style|refactor|perf|test|build|ci|chore|revert|poc)"
17+
local scope_regex="\([[:alnum:]-]+\)"
18+
local breaking_change="!"
19+
local description_regex=": .+"
20+
21+
# Check if message is empty
22+
if [ -z "$message" ]; then
23+
print_error "Commit message cannot be empty"
24+
fi
25+
26+
# Split message into lines
27+
IFS=$'\n' read -rd '' -a lines <<<"$message"
28+
local first_line="${lines[0]}"
29+
30+
# Validate first line format
31+
if ! [[ "$first_line" =~ $type_regex ]]; then
32+
print_error "Commit message must start with a valid type (feat, fix, docs, etc.)"
33+
fi
34+
35+
# Extract the prefix (everything before the colon)
36+
local prefix="${first_line%%:*}"
37+
38+
# Check if scope is present and valid (if included)
39+
if [[ "$prefix" =~ \( ]]; then
40+
if ! [[ "$prefix" =~ $type_regex$scope_regex(!)?$ ]]; then
41+
print_error "Invalid scope format in commit message"
42+
fi
43+
fi
44+
45+
# Check for breaking change indicator
46+
if [[ "$prefix" =~ \!$ ]] && ! [[ "$prefix" =~ $type_regex($scope_regex)?\!$ ]]; then
47+
print_error "Invalid breaking change format in commit message"
48+
fi
49+
50+
# Validate description presence
51+
if ! [[ "$first_line" =~ $description_regex ]]; then
52+
print_error "Commit message must have a description after the type/scope"
53+
fi
54+
55+
# Validate body format if present
56+
if [ ${#lines[@]} -gt 1 ]; then
57+
# Check if there's a blank line between subject and body
58+
if [ -n "${lines[1]}" ]; then
59+
print_error "There must be a blank line between subject and body"
60+
fi
61+
62+
# Validate footer format if present
63+
for ((i=2; i<${#lines[@]}; i++)); do
64+
local line="${lines[i]}"
65+
# Skip empty lines
66+
[ -z "$line" ] && continue
67+
68+
# Check for footer format (if it looks like a footer)
69+
if [[ "$line" =~ ^[A-Z][A-Z0-9-]*:[[:space:]] ]] || [[ "$line" =~ ^BREAKING[[:space:]]CHANGE:[[:space:]] ]]; then
70+
# Valid footer format found, continue to next line
71+
continue
72+
fi
73+
74+
# If line starts with what looks like a footer token but doesn't match the format, it's invalid
75+
if [[ "$line" =~ ^[A-Z][A-Z0-9-]*[^:].*$ ]] || [[ "$line" =~ ^BREAKING[[:space:]]CHANGE[^:].*$ ]]; then
76+
print_error "Invalid footer format: $line"
77+
fi
78+
done
79+
fi
80+
81+
echo "Commit message is valid!"
82+
exit 0
83+
}
84+
85+
# Main execution
86+
if [ -z "$1" ]; then
87+
# If no argument provided, try to read from .git/COMMIT_EDITMSG
88+
if [ -f .git/COMMIT_EDITMSG ]; then
89+
commit_msg=$(cat .git/COMMIT_EDITMSG)
90+
else
91+
print_error "No commit message provided and .git/COMMIT_EDITMSG not found"
92+
fi
93+
else
94+
commit_msg="$1"
95+
fi
96+
97+
validate_commit_message "$commit_msg"

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,17 @@
7272
"@php artisan config:clear --ansi",
7373
"@php artisan test"
7474
],
75+
"prepare": "git config core.hookspath .githooks",
7576
"fmt": "vendor/bin/pint -vv",
7677
"fmt:test": "vendor/bin/pint --test",
7778
"lint": "XDEBUG_MODE=off vendor/bin/phpstan analyze",
7879
"refactor": "vendor/bin/rector process",
7980
"refactor:test": "vendor/bin/rector process --dry-run",
8081
"typos": "vendor/bin/peck",
82+
"fix": [
83+
"@fmt",
84+
"@refactor"
85+
],
8186
"ci": [
8287
"@lint",
8388
"@fmt:test",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"fmt:check": "prettier --check resources/",
1010
"lint": "eslint . --fix",
1111
"lint:check": "eslint .",
12-
"check": "npm run fmt:check && npm run lint:check"
12+
"check": "npm run fmt:check && npm run lint:check",
13+
"fix": "npm run fmt && npm run lint"
1314
},
1415
"devDependencies": {
1516
"@eslint/js": "^9.19.0",

0 commit comments

Comments
 (0)