Project 05 — Git Hook Collection¶
Build a set of git hooks that run ShellCheck, check for trailing whitespace, validate commit message format, and block dangerous commits — enforcing quality before code ever leaves your machine.
What You Will Build¶
A collection of git hooks:
- pre-commit — runs ShellCheck on staged .sh files, checks for trailing whitespace, blocks large files
- commit-msg — validates commit message format (imperative mood, length limits, ticket ID)
- pre-push — runs the full test suite before allowing a push
- An install.sh that sets up the hooks in any repository
How Git Hooks Work¶
Hooks are shell scripts in .git/hooks/. Git runs them automatically at key points. Make them executable (chmod +x) and they run.
.git/hooks/
pre-commit ← runs before a commit is created
commit-msg ← runs to validate the commit message
pre-push ← runs before a push to remote
post-merge ← runs after a git merge
The hook's exit code matters: 0 = allow the operation, non-zero = block it.
Getting Started¶
The pre-commit Hook¶
#!/usr/bin/env bash
set -euo pipefail
# Run ShellCheck on staged .sh files
staged_sh=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true)
if [[ -n "$staged_sh" ]]; then
echo "Running ShellCheck on staged scripts..."
echo "$staged_sh" | xargs shellcheck || {
echo "ShellCheck failed. Fix the issues above before committing."
exit 1
}
fi
# Check for trailing whitespace
if git diff --cached --check; then
: # no trailing whitespace
else
echo "ERROR: Staged files contain trailing whitespace."
echo "Fix with: git diff --cached --check"
exit 1
fi
# Block files over 1MB
large_files=$(git diff --cached --name-only | xargs -I{} sh -c '[ -f "{}" ] && find "{}" -size +1M -print' 2>/dev/null || true)
if [[ -n "$large_files" ]]; then
echo "ERROR: Large files staged for commit:"
echo "$large_files"
exit 1
fi
The commit-msg Hook¶
#!/usr/bin/env bash
MSG_FILE="$1"
MSG=$(cat "$MSG_FILE")
FIRST_LINE="${MSG%%$'\n'*}"
# Must be at least 10 characters
if [[ ${#FIRST_LINE} -lt 10 ]]; then
echo "ERROR: Commit message too short (min 10 chars)"
exit 1
fi
# Must not exceed 72 characters
if [[ ${#FIRST_LINE} -gt 72 ]]; then
echo "ERROR: First line too long (max 72 chars, got ${#FIRST_LINE})"
exit 1
fi
# Must not start with capital letter (enforce lowercase imperative)
if [[ "${FIRST_LINE:0:1}" =~ [A-Z] ]]; then
echo "WARN: Commit messages should start with a lowercase verb (e.g., 'add', 'fix', 'update')"
fi
install.sh¶
#!/usr/bin/env bash
set -euo pipefail
HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for hook in pre-commit commit-msg pre-push; do
[-f "$SCRIPT_DIR/$hook"](<../../-f "$SCRIPT_DIR/$hook".md>) || continue
cp "$SCRIPT_DIR/$hook" "$HOOKS_DIR/$hook"
chmod +x "$HOOKS_DIR/$hook"
echo "Installed: $hook"
done
echo "Done. Run 'git commit' to test."
Stretch Goals¶
- Add a
pre-pushhook that runsbatstest files if they exist - Add branch name validation (e.g., must match
feature/,fix/,chore/) - Publish the hook collection as a standalone repo with a one-line install command
[[04-backup-script]] | [[06-menu-cli-app]]