diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..d017a90eb --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,25 @@ +#!/bin/bash + +# . "$(dirname -- "$0")/_/husky.sh" + +# ANSI colors +RED="\033[0;31m" +YELLOW="\033[0;33m" +CYAN="\033[0;36m" +GREEN="\033[0;32m" +RESET="\033[0m" + +commit_msg_file=$1 + +# Guard: ensure commit message file exists +if [ -z "$commit_msg_file" ] || [ ! -f "$commit_msg_file" ]; then + echo "âš ī¸ No commit message file provided or file does not exist." + echo "👉 Skipping DCO sign-off check." + exit 0 +fi + +if ! grep -q "Signed-off-by:" "$commit_msg_file"; then + echo "${RED} Commit message missing Signed-off-by line ${RESET}" + echo "${GREEN} Use: git commit -s ... ${RESET}" + exit 1 +fi \ No newline at end of file diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100755 index 000000000..8f919d7a3 --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1,43 @@ +#!/bin/bash + +RESET="\033[0m" +RED="\033[0;31m" +GREEN="\033[0;32m" +YELLOW="\033[0;33m" +BLUE="\033[0;34m" +CYAN="\033[0;36m" + +# . "$(dirname -- "$0")/_/husky.sh" + +echo "${BLUE}Checking last commit signature...${RESET}" + +last_commit=$(git rev-parse HEAD) + +# Count parents to detect merge commits +parent_count=$(git rev-list --parents -n 1 $last_commit | awk '{print NF-1}') + +if [ "$parent_count" -gt 1 ]; then + echo "${BLUE}Skipping merge commit $last_commit${RESET}" + exit 0 +fi + +sig_status=$(git log --format='%G?' -n 1 $last_commit) + +if [ "$sig_status" != "G" ]; then + echo "${RED}❌ Commit $last_commit is not verified!${RESET}" + echo + echo "${YELLOW}👉 How to fix:${RESET}" + echo " 1. Re-sign your last commit with both sign-off (-s) and signature (-S):" + echo " git commit --amend -sS --no-edit" + echo + echo " 2. If you haven't set up commit signing yet:" + echo " - GPG/SSH signing setup: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits" + echo + echo "${CYAN}💡 Tip: You can enable automatic signing for all commits with:${RESET}" + echo " git config --global commit.gpgsign true" + echo + exit 1 + # In post-commit we usually *warn* not block, but you can `exit 1` if you want strict enforcement +else + echo "${GREEN}Commit $last_commit is signed and verified.${RESET}" +fi \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 5e5a32637..b55873f59 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,5 @@ -#!/bin/sh -cd "$(dirname "$0")/.." # Move to project root (outer folder) -# cd nextjs # Move into Next.js folder -npx lint-staged +#!/usr/bin/env sh + +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..40a64eb78 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,144 @@ +#!/bin/bash + +# . "$(dirname -- "$0")/_/husky.sh" + +# ANSI colors +RED="\033[0;31m" +YELLOW="\033[0;33m" +CYAN="\033[0;36m" +GREEN="\033[0;32m" +RESET="\033[0m" + +echo "🔒 Checking commit signatures..." + +# Read stdin into a temp file +TMP_FILE=$(mktemp) +cat > "$TMP_FILE" + +if [ ! -s "$TMP_FILE" ]; then + echo "No refs to push - skipping commit signature validation." +fi + +while read local_ref local_sha remote_ref remote_sha; do + # Determine range of commits + if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then + # Use the branch being pushed (may differ from HEAD) + if echo "$local_ref" | grep -q '^refs/heads/'; then + target_branch=${local_ref#refs/heads/} + else + target_branch=$(git rev-parse --abbrev-ref HEAD) + fi + + # Find closest local base branch by commit distance (exclude the target itself) + base_branch=$( + git for-each-ref --format='%(refname:short)' refs/heads/ \ + | grep -vxF "$target_branch" \ + | while IFS= read -r other_branch; do + merge_base=$(git merge-base "$target_branch" "$other_branch") || continue + [ -n "$merge_base" ] || continue + echo "$(git rev-list --count "${merge_base}..${target_branch}") $other_branch" + done \ + | sort -n \ + | head -n1 \ + | awk '{print $2}' + ) + + if [ -z "$base_branch" ]; then + # Resolve remote default (origin/HEAD) or use $default_branch/main + base_branch=${default_branch:-$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')} + [ -n "$base_branch" ] || base_branch=main + echo "âš ī¸ Base branch not found — defaulting to '${base_branch}'" + fi + + # Prefer local base, else remote tracking + if git show-ref --verify --quiet "refs/heads/$base_branch"; then + base_ref="$base_branch" + else + base_ref="origin/$base_branch" + fi + + base_commit=$(git merge-base "$base_ref" "$target_branch" 2>/dev/null || true) + if [ -n "$base_commit" ]; then + range="$base_commit..$local_sha" + else + # Safe minimal fallback: just the tip commit + range="$local_sha" + fi + else + range="$remote_sha..$local_sha" + fi + + unsigned_commits="" + + echo "commit range ${range}" + # Loop through each commit in range + for commit in $(git rev-list $range); do + # Skip merge commits + parent_count=$(git rev-list --parents -n 1 $commit | awk '{print NF-1}') + if [ "$parent_count" -gt 1 ]; then + echo "${CYAN}â„šī¸ Skipping merge commit $commit${RESET}" + continue + fi + sig_status=$(git log --format='%G?' -n 1 $commit) + # Currently only check for "N" - for no signature. + if [ "$sig_status" = "N" ]; then + unsigned_commits="$unsigned_commits $commit" + fi + done + + # If there are unsigned commits, prompt the developer + if [ -n "$unsigned_commits" ]; then + echo "${RED}❌ Found unsigned commits:${RESET} $unsigned_commits" + echo + echo "${YELLOW}Do you want to automatically sign these commits? [y/N]${RESET}" + read -r answer " + echo + exit 1 + fi + fi +done < "$TMP_FILE" + +rm -f "$TMP_FILE" + +echo "${GREEN}✅ No unsigned commits found${RESET}" diff --git a/package.json b/package.json index 1fd134440..9445390c6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint:strict": "eslint --max-warnings=0 src", "format": "prettier --write .", "format:check": "prettier -c -w .", - "prepare": "husky install" + "prepare": "husky" }, "dependencies": { "@dnd-kit/core": "^6.3.1",