Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 7 additions & 32 deletions hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,38 +1,13 @@
#!/bin/sh
# pre-commit hook: hygiene checks + shellcheck
# Checks (ordered fastest-first):
# 1. Branch ancestry (squad/* must descend from develop)
# 2. ASCII-only on staged *.ps1, *.md, *.sh files
# 3. Refuse commits directly on develop/main/master
# 4. Shellcheck on staged .sh files
# 1. ASCII-only on staged *.ps1, *.md, *.sh files
# 2. Refuse commits directly on develop/main/master
# 3. Shellcheck on staged .sh files
set -e

# ---------------------------------------------------------------------------
# Check 1: Branch ancestry (fast -- single git command)
# ---------------------------------------------------------------------------
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
case "$CURRENT_BRANCH" in
squad/*|goofy/*|mickey/*|chip/*|pluto/*|donald/*|jiminy/*)
if git rev-parse --verify develop >/dev/null 2>&1; then
if ! git merge-base --is-ancestor develop HEAD 2>/dev/null; then
echo ""
echo "ERROR: Branch ancestry bleed detected."
echo ""
echo " Branch '$CURRENT_BRANCH' is not descended from 'develop'."
echo " squad/* branches must be forked from develop, not from other squad branches."
echo ""
echo " Fix: rebase onto develop or recreate the branch from develop."
echo " git rebase develop"
echo " # or: git checkout develop && git checkout -b <new-branch>"
echo ""
exit 1
fi
fi
;;
esac

# ---------------------------------------------------------------------------
# Check 2: ASCII-only on staged *.ps1, *.md, *.sh files
# Check 1: ASCII-only on staged *.ps1, *.md, *.sh files
#
# Scanned extensions:
# .ps1 -- PS 5.1 on Windows uses CP1252; non-ASCII breaks string literals
Expand Down Expand Up @@ -74,22 +49,22 @@ if [ -n "$ASCII_STAGED" ]; then
fi

# ---------------------------------------------------------------------------
# Check 3: Refuse commits directly on develop / main / master
# Check 2: Refuse commits directly on develop / main / master
# ---------------------------------------------------------------------------
current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
case "$current_branch" in
develop|main|master)
echo ""
echo "ERROR: refusing to commit directly on '$current_branch'."
echo " Create a feature branch first:"
echo " git checkout -b squad/<issue>-<slug>"
echo " git checkout -b feat/<issue>-<slug>"
echo ""
exit 1
;;
esac

# ---------------------------------------------------------------------------
# Check 4: Shellcheck on staged .sh files
# Check 3: Shellcheck on staged .sh files
# ---------------------------------------------------------------------------
if ! command -v shellcheck >/dev/null 2>&1; then
exit 0
Expand Down
114 changes: 49 additions & 65 deletions tests/test_precommit_hygiene.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
# Tests for pre-commit hygiene checks (Issue #240) and pre-push guard (Issue #224)
#
# Covers:
# Check 1: Branch ancestry (squad/* must descend from develop)
# Check 2: ASCII-only on staged *.ps1, *.md, *.sh files
# Check 3: Protected branch refuse (develop/main/master)
# Check 4: Shellcheck on staged .sh files
# Check 1: ASCII-only on staged *.ps1, *.md, *.sh files
# Check 2: Protected branch refuse (develop/main/master)
# Check 3: Shellcheck on staged .sh files
# pre-push: Main push guard + advisory PSScriptAnalyzer exit-code
#
# Usage:
Expand All @@ -31,7 +30,7 @@ mkdir -p "$TMPDIR_BASE"
cleanup() { rm -rf "$TMPDIR_BASE"; }
trap cleanup EXIT

# Helper: create a fresh git repo with develop branch and a squad branch
# Helper: create a fresh git repo with develop branch and a feature branch
setup_test_repo() {
local repo_dir="$1"
mkdir -p "$repo_dir"
Expand All @@ -51,118 +50,103 @@ setup_test_repo() {
}

# ===========================================================================
# Check 1 Tests: Branch ancestry (removed - squad-specific)
# Check 1 Tests: ASCII-only on staged .ps1 / .md / .sh files
# ===========================================================================
echo ""
echo "=== Check 1: Branch ancestry (SKIPPED - no longer applicable) ==="
echo "=== Check 1: ASCII-only .ps1 / .md / .sh ==="

# Test 1a: SKIPPED - squad branch ancestry check was squad-specific
pass "T1a: Branch ancestry check removed (squad-specific)"

# Test 1b: SKIPPED
pass "T1b: Branch ancestry check removed (squad-specific)"

# Test 1c: SKIPPED
pass "T1c: Branch ancestry check removed (squad-specific)"

# ===========================================================================
# Check 2 Tests: ASCII-only on staged .ps1 / .md / .sh files
# ===========================================================================
echo ""
echo "=== Check 2: ASCII-only .ps1 / .md / .sh ==="

# Test 2a: FAIL - .ps1 with non-ASCII
T2A_DIR="${TMPDIR_BASE}/t2a"
setup_test_repo "$T2A_DIR"
# Test 1a: FAIL - .ps1 with non-ASCII
T1A_DIR="${TMPDIR_BASE}/t1a"
setup_test_repo "$T1A_DIR"
git checkout -q -b feature/ascii-test
# Create a .ps1 with an em dash (UTF-8 bytes for U+2014: E2 80 94)
printf 'Write-Host "hello \xe2\x80\x94 world"\n' > test.ps1
git add test.ps1
if sh "$HOOK" >/dev/null 2>&1; then
fail "T2a: .ps1 with non-ASCII bytes should fail"
fail "T1a: .ps1 with non-ASCII bytes should fail"
else
pass "T2a: .ps1 with non-ASCII bytes fails"
pass "T1a: .ps1 with non-ASCII bytes fails"
fi

# Test 2b: PASS - .ps1 with only ASCII
T2B_DIR="${TMPDIR_BASE}/t2b"
setup_test_repo "$T2B_DIR"
# Test 1b: PASS - .ps1 with only ASCII
T1B_DIR="${TMPDIR_BASE}/t1b"
setup_test_repo "$T1B_DIR"
git checkout -q -b feature/ascii-pass
echo 'Write-Host "hello -- world"' > test.ps1
git add test.ps1
if sh "$HOOK" >/dev/null 2>&1; then
pass "T2b: .ps1 with ASCII-only passes"
pass "T1b: .ps1 with ASCII-only passes"
else
fail "T2b: .ps1 with ASCII-only passes"
fail "T1b: .ps1 with ASCII-only passes"
fi

# Test 2c: FAIL - .md with non-ASCII (em-dash) is now rejected (#322 part B)
T2C_DIR="${TMPDIR_BASE}/t2c"
setup_test_repo "$T2C_DIR"
# Test 1c: FAIL - .md with non-ASCII (em-dash) is now rejected (#322 part B)
T1C_DIR="${TMPDIR_BASE}/t1c"
setup_test_repo "$T1C_DIR"
git checkout -q -b feature/md-nonascii
printf 'hello \xe2\x80\x94 world\n' > notes.md
git add notes.md
if sh "$HOOK" >/dev/null 2>&1; then
fail "T2c: .md with non-ASCII bytes should fail"
fail "T1c: .md with non-ASCII bytes should fail"
else
pass "T2c: .md with non-ASCII bytes fails"
pass "T1c: .md with non-ASCII bytes fails"
fi

# Test 2d: FAIL - .sh with non-ASCII (em-dash) is rejected (#322 part B)
T2D_DIR="${TMPDIR_BASE}/t2d"
setup_test_repo "$T2D_DIR"
# Test 1d: FAIL - .sh with non-ASCII (em-dash) is rejected (#322 part B)
T1D_DIR="${TMPDIR_BASE}/t1d"
setup_test_repo "$T1D_DIR"
git checkout -q -b feature/sh-nonascii
printf 'echo "hello \xe2\x80\x94 world"\n' > script.sh
git add script.sh
if sh "$HOOK" >/dev/null 2>&1; then
fail "T2d: .sh with non-ASCII bytes should fail"
fail "T1d: .sh with non-ASCII bytes should fail"
else
pass "T2d: .sh with non-ASCII bytes fails"
pass "T1d: .sh with non-ASCII bytes fails"
fi

# Test 2e: PASS - non-scanned extension (.txt) with non-ASCII is allowed
T2E_DIR="${TMPDIR_BASE}/t2e"
setup_test_repo "$T2E_DIR"
# Test 1e: PASS - non-scanned extension (.txt) with non-ASCII is allowed
T1E_DIR="${TMPDIR_BASE}/t1e"
setup_test_repo "$T1E_DIR"
git checkout -q -b feature/txt-nonascii
printf 'hello \xe2\x80\x94 world\n' > notes.txt
git add notes.txt
if sh "$HOOK" >/dev/null 2>&1; then
pass "T2e: non-scanned extension (.txt) with non-ASCII is allowed"
pass "T1e: non-scanned extension (.txt) with non-ASCII is allowed"
else
fail "T2e: non-scanned extension (.txt) with non-ASCII is allowed"
fail "T1e: non-scanned extension (.txt) with non-ASCII is allowed"
fi

# ===========================================================================
# Check 3 Tests: Refuse commits on protected branches
# Check 2 Tests: Refuse commits on protected branches
# ===========================================================================
echo ""
echo "=== Check 3: Protected branch refuse ==="
echo "=== Check 2: Protected branch refuse ==="

# Test 3a: FAIL - commit on develop should be refused
# Test 2a: FAIL - commit on develop should be refused
T5A_DIR="${TMPDIR_BASE}/t5a"
setup_test_repo "$T5A_DIR"
# Already on develop after setup_test_repo
echo "bad" > bad.txt
git add bad.txt
if sh "$HOOK" >/dev/null 2>&1; then
fail "T3a: commit on develop should be refused"
fail "T2a: commit on develop should be refused"
else
pass "T3a: commit on develop is refused"
pass "T2a: commit on develop is refused"
fi

# Test 3b: FAIL - commit on main should be refused
# Test 2b: FAIL - commit on main should be refused
T5B_DIR="${TMPDIR_BASE}/t5b"
setup_test_repo "$T5B_DIR"
git checkout -q -b main
echo "bad" > bad.txt
git add bad.txt
if sh "$HOOK" >/dev/null 2>&1; then
fail "T3b: commit on main should be refused"
fail "T2b: commit on main should be refused"
else
pass "T3b: commit on main is refused"
pass "T2b: commit on main is refused"
fi

# Test 3c: FAIL - commit on master should be refused
# Test 2c: FAIL - commit on master should be refused
T5C_DIR="${TMPDIR_BASE}/t5c"
mkdir -p "$T5C_DIR"
cd "$T5C_DIR"
Expand All @@ -177,33 +161,33 @@ git branch -m master 2>/dev/null || true
echo "bad" > bad.txt
git add bad.txt
if sh "$HOOK" >/dev/null 2>&1; then
fail "T3c: commit on master should be refused"
fail "T2c: commit on master should be refused"
else
pass "T3c: commit on master is refused"
pass "T2c: commit on master is refused"
fi

# Test 3d: PASS - commit on feature branch is allowed
# Test 2d: PASS - commit on feature branch is allowed
T5D_DIR="${TMPDIR_BASE}/t5d"
setup_test_repo "$T5D_DIR"
git checkout -q -b feature/123-feature
echo "good" > good.txt
git add good.txt
if sh "$HOOK" >/dev/null 2>&1; then
pass "T3d: commit on feature branch is allowed"
pass "T2d: commit on feature branch is allowed"
else
fail "T3d: commit on feature branch is allowed"
fail "T2d: commit on feature branch is allowed"
fi

# Test 3e: PASS - commit on pluto/* branch is allowed
# Test 2e: PASS - commit on feature branch is allowed
T5E_DIR="${TMPDIR_BASE}/t5e"
setup_test_repo "$T5E_DIR"
git checkout -q -b pluto/249-fix
git checkout -q -b feat/249-fix
echo "good" > good.txt
git add good.txt
if sh "$HOOK" >/dev/null 2>&1; then
pass "T3e: commit on pluto/* branch is allowed"
pass "T2e: commit on feature branch is allowed"
else
fail "T3e: commit on pluto/* branch is allowed"
fail "T2e: commit on feature branch is allowed"
fi

# ===========================================================================
Expand Down
10 changes: 5 additions & 5 deletions tests/test_windows_setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ if (-not (Get-Command sh -ErrorAction SilentlyContinue)) {
New-YTestRepo $repoDir
Push-Location $repoDir
try {
& git checkout -q -b pluto/ascii-fail 2>&1 | Out-Null
& git checkout -q -b feat/ascii-fail 2>&1 | Out-Null
# Write a .ps1 containing a UTF-8 em-dash (bytes: 0xE2 0x80 0x94)
$before = [System.Text.Encoding]::ASCII.GetBytes('Write-Host "hello ')
$emDash = [byte[]](0xE2, 0x80, 0x94)
Expand All @@ -1546,7 +1546,7 @@ if (-not (Get-Command sh -ErrorAction SilentlyContinue)) {
New-YTestRepo $repoDir
Push-Location $repoDir
try {
& git checkout -q -b pluto/ascii-pass 2>&1 | Out-Null
& git checkout -q -b feat/ascii-pass 2>&1 | Out-Null
'Write-Host "hello -- world"' | Set-Content "test.ps1" -Encoding ASCII
& git add "test.ps1" 2>&1 | Out-Null
$out = & sh $preCommitHook 2>&1
Expand Down Expand Up @@ -1586,7 +1586,7 @@ if (-not (Get-Command sh -ErrorAction SilentlyContinue)) {
try {
$hookUnix = $prePushHook.Replace('\', '/')
$stdinFile = Join-Path $yTmpBase "push_stdin_y5.txt"
Set-Content $stdinFile "refs/heads/squad/224-test abc1234 refs/heads/develop def5678" -Encoding ASCII
Set-Content $stdinFile "refs/heads/feat/224-test abc1234 refs/heads/develop def5678" -Encoding ASCII
$stdinUnix = $stdinFile.Replace('\', '/')
$out = & sh -c "sh '$hookUnix' origin 'https://github.com/test/repo' < '$stdinUnix'" 2>&1
if ($LASTEXITCODE -ne 0) {
Expand All @@ -1604,14 +1604,14 @@ if (-not (Get-Command sh -ErrorAction SilentlyContinue)) {
New-YTestRepo $repoDir
Push-Location $repoDir
try {
& git checkout -q -b squad/224-advisory-test 2>&1 | Out-Null
& git checkout -q -b feat/224-advisory-test 2>&1 | Out-Null
# Commit a .ps1 so the hook has content to (optionally) analyze
'Write-Host "advisory test"' | Set-Content "advisory.ps1" -Encoding ASCII
& git add "advisory.ps1" 2>&1 | Out-Null
& git commit -q -m "test: advisory ps1" 2>&1 | Out-Null
$hookUnix = $prePushHook.Replace('\', '/')
$stdinFile = Join-Path $yTmpBase "push_stdin_y6.txt"
Set-Content $stdinFile "refs/heads/squad/224-advisory-test abc1234 refs/heads/squad/224-advisory-test def5678" -Encoding ASCII
Set-Content $stdinFile "refs/heads/feat/224-advisory-test abc1234 refs/heads/feat/224-advisory-test def5678" -Encoding ASCII
$stdinUnix = $stdinFile.Replace('\', '/')
$out = & sh -c "sh '$hookUnix' origin 'https://github.com/test/repo' < '$stdinUnix'" 2>&1
if ($LASTEXITCODE -ne 0) {
Expand Down
Loading