224 lines
8.6 KiB
Bash
Executable File
224 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression matrix for git-guard.py: `bash ~/.claude/hooks/git-guard-test.sh` (exit 1 on mismatch).
|
|
# Kept as a file on purpose: typed inline, the very commands under test would trip the live hook.
|
|
G="$(dirname "$(readlink -f "$0")")/git-guard.py"
|
|
fail=0
|
|
# No exempt repository for the rows that do not ask for one, whatever this host's ~/.claude/git-guard-exempt says.
|
|
export GIT_GUARD_EXEMPT=""
|
|
# `git reset <one arg>` is allowed only for an existing path: give the matrix one to point at.
|
|
tmp=$(mktemp -d) && cd "$tmp" && touch existing.py
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
run() {
|
|
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null
|
|
rc=$?
|
|
got=allow
|
|
[ $rc -eq 2 ] && got=block
|
|
[ "$got" = "$1" ] && mark=ok || {
|
|
mark=MISMATCH
|
|
fail=1
|
|
}
|
|
printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"
|
|
}
|
|
echo "--- must block: commits / history ---"
|
|
run block 'git push'
|
|
run block 'git -C /home/coja/.dots push origin main'
|
|
run block 'git -c user.name=x commit -m fix'
|
|
run block '/usr/bin/git push'
|
|
run block 'env GIT_DIR=.git git push'
|
|
run block 'timeout 5 git push'
|
|
run block 'sudo -u me git push'
|
|
run block 'cd /tmp && git push'
|
|
run block 'git status; git commit -am wip'
|
|
run block 'x=1; git push'
|
|
run block 'git push 2>&1 | tee log'
|
|
run block "sh -c 'git push origin HEAD'"
|
|
run block 'eval "git push"'
|
|
run block 'git -C $(pwd) push'
|
|
run block 'echo "$(git push)"'
|
|
run block 'git --git-dir=/x/.git --work-tree=/x push'
|
|
run block 'git commit -m "fix: grep pattern; not a command"'
|
|
run block 'git revert HEAD'
|
|
run block 'git cherry-pick abc123'
|
|
run block 'git rebase -i HEAD~3'
|
|
run block 'git rebase --interactive main'
|
|
run block 'git -C . rebase -i --autosquash main'
|
|
run block 'git rebase --autostash origin/main'
|
|
run block 'git -c rebase.autoStash=true rebase main'
|
|
run block 'git merge feature'
|
|
run block 'git merge --no-ff feature'
|
|
echo "--- must block: discards working-tree changes ---"
|
|
run block 'git reset --hard HEAD~1'
|
|
run block 'git reset HEAD~1 --hard'
|
|
run block 'git reset --soft HEAD~1'
|
|
run block 'git reset --keep HEAD~2'
|
|
run block 'git reset --merge'
|
|
run block 'git reset'
|
|
run block 'git reset -q'
|
|
run block 'git reset HEAD'
|
|
run block 'git reset HEAD~1'
|
|
run block 'git reset abc1234'
|
|
run block 'git reset 0123456789abcdef0123456789abcdef01234567'
|
|
run block 'git reset origin/main'
|
|
run block 'git reset @{u}'
|
|
run block 'git restore --staged .'
|
|
run block 'git restore --staged :/'
|
|
run block 'git clean -fd'
|
|
run block 'git -C . clean -n'
|
|
run block 'git restore file.py'
|
|
run block 'git restore --staged --worktree file.py'
|
|
run block 'git restore -W file.py'
|
|
run block 'git checkout -- .'
|
|
run block 'git checkout -- file.py'
|
|
run block 'git checkout HEAD -- file.py'
|
|
run block 'git checkout .'
|
|
run block 'git checkout -f main'
|
|
run block 'git stash drop'
|
|
run block 'git stash pop'
|
|
run block 'git stash clear'
|
|
run block 'git branch -D feature'
|
|
run block 'git branch -f main HEAD~1'
|
|
run block 'git branch -M main trunk'
|
|
echo "--- must block: gh pr write ops (create only with --head, edit only title/body) ---"
|
|
run block 'gh pr merge 12 --squash'
|
|
run block 'gh pr close 4'
|
|
run block 'gh -R o/r pr create --fill'
|
|
run block 'gh pr create --title x --body y'
|
|
run block 'gh pr create --dry-run --fill'
|
|
run block 'gh pr edit 3 --add-reviewer bob'
|
|
run block 'gh pr edit 3 --title x --base main'
|
|
run block 'gh pr edit --add-label=bug 3'
|
|
run block 'gh pr edit 3 -b y -m v2'
|
|
echo "--- must allow ---"
|
|
run allow 'git status'
|
|
run allow 'git log -1 --oneline'
|
|
run allow 'git diff --cached --stat'
|
|
run allow 'git add -A common/.config/claude'
|
|
run allow 'git rev-parse --git-path CLAUDE_COMMIT_MSG'
|
|
run allow 'git -c core.pager=cat log -3'
|
|
run allow 'git reset -- file.txt'
|
|
run allow 'git reset HEAD file.txt'
|
|
run allow 'git reset HEAD existing.py other.py'
|
|
run allow 'git reset existing.py'
|
|
run allow 'git reset -p'
|
|
run allow 'git reset -- .'
|
|
run allow 'git restore --staged existing.py'
|
|
run allow 'git restore --staged file.py'
|
|
run allow 'git restore -S file.py'
|
|
run allow 'git checkout main'
|
|
run allow 'git checkout -b feature'
|
|
run allow 'git switch main'
|
|
run allow 'git merge --abort'
|
|
run allow 'git rebase main'
|
|
run allow 'git rebase origin/main'
|
|
run allow 'git rebase -Xtheirs main'
|
|
run allow 'git rebase --continue'
|
|
run allow 'git rebase --abort'
|
|
run allow 'git -c core.editor=true rebase --continue'
|
|
run allow 'GIT_EDITOR=true git rebase --continue'
|
|
run allow 'git stash'
|
|
run allow 'git stash list'
|
|
run allow 'git stash push -m wip'
|
|
run allow 'git stash show -p'
|
|
run allow 'git branch -d merged-branch'
|
|
run allow 'git branch --list'
|
|
run allow 'git branch -vv'
|
|
run allow 'git commit-tree HEAD^{tree} -m x'
|
|
run allow 'git worktree list'
|
|
run allow 'grep -rn "git push" README.md'
|
|
run allow 'grep -n "foo; git commit" file'
|
|
run allow "echo 'git push'"
|
|
run allow 'echo "run git push"'
|
|
run allow "cat > README.md <<'EOF'
|
|
Deploy with:
|
|
git status; git commit -am x; git push origin main
|
|
EOF
|
|
echo written"
|
|
run allow 'python3 - <<EOF
|
|
print("git commit is manual")
|
|
EOF'
|
|
run allow 'gh pr view 12 --json updatedAt'
|
|
run allow 'gh pr create --head feat --title x --body y'
|
|
run allow 'gh pr create -H feat --fill'
|
|
run allow 'gh -R o/r pr create --head=feat --fill'
|
|
run allow 'gh pr create --help'
|
|
run allow 'gh pr edit 3 --title x'
|
|
run allow 'gh pr edit 3 -t x -b "- item --base"'
|
|
run allow 'gh pr edit --body-file - 12'
|
|
run allow 'gh pr edit 12 -F body.md --repo o/r'
|
|
run allow 'gh pr edit 12 --title=x -bBody'
|
|
run allow 'gh workflow run ci.yml --ref main'
|
|
run allow 'gh run rerun 123 --failed'
|
|
run allow 'gh api graphql -f query="mutation{...}"'
|
|
run allow 'gh auth status'
|
|
run allow 'ls -la HEAD#x; git status'
|
|
run allow 'git log --grep="git push" -5'
|
|
run allow '~/.config/git/claude-commit-msg list'
|
|
echo "--- exempt repo (GIT_GUARD_EXEMPT = a scratch repo; commit and merge pass there, nothing else) ---"
|
|
# runin <expect> <cwd> <command>: the hook input carries the session's cwd, as Claude Code sends it.
|
|
R=$tmp/repo
|
|
WT=$tmp/repo-wt
|
|
git init -q "$R" && git -C "$R" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init \
|
|
&& git -C "$R" worktree add -q "$WT" 2>/dev/null && mkdir -p "$R/sub" && git init -q "$tmp/other"
|
|
runin() {
|
|
printf '%s' "$(python3 -c 'import json,sys; print(json.dumps({"tool_name":"Bash","tool_input":{"command":sys.argv[1]},"cwd":sys.argv[2]}))' "$3" "$2")" | GIT_GUARD_EXEMPT=$R python3 "$G" 2>/dev/null
|
|
rc=$?
|
|
got=allow
|
|
[ $rc -eq 2 ] && got=block
|
|
[ "$got" = "$1" ] && mark=ok || {
|
|
mark=MISMATCH
|
|
fail=1
|
|
}
|
|
printf '%-8s %-6s %-6s [%s] %s\n' "$mark" "$1" "$got" "${2#"$tmp"/}" "$(printf '%s' "$3" | head -1)"
|
|
}
|
|
runin allow "$R" 'git commit -m "docs: x"'
|
|
runin allow "$R" 'git commit -m x -- docs/usage.md'
|
|
runin allow "$R" 'git commit -m"fix a bug"'
|
|
runin allow "$R" 'git commit -m "-a is not the flag here"'
|
|
runin allow "$R" 'git commit --allow-empty -m x'
|
|
runin allow "$R/sub" 'git commit -m x'
|
|
runin allow "$R" 'git merge --ff-only feature'
|
|
runin allow "$R" 'git merge --no-ff feature'
|
|
runin allow "$WT" 'git commit -m x'
|
|
runin allow "$tmp" "git -C $WT commit -m x"
|
|
runin allow "$tmp" "cd $R && git commit -m x"
|
|
runin allow "$tmp" 'cd repo && git commit -m x'
|
|
runin allow "$tmp" "cd $WT; git status; git merge main"
|
|
runin allow "$R" "sh -c 'git commit -m x'"
|
|
runin block "$R" 'git push'
|
|
runin block "$R" 'git commit --amend --no-edit'
|
|
runin block "$R" 'git commit -am wip'
|
|
runin block "$R" 'git commit -qa -m wip'
|
|
runin block "$R" 'git commit --all -m wip'
|
|
runin block "$R" 'git rebase -i main'
|
|
runin block "$R" 'git cherry-pick abc123'
|
|
runin block "$R" 'git revert HEAD'
|
|
runin block "$R" 'git reset --hard HEAD~1'
|
|
runin block "$R" 'git checkout -- file.py'
|
|
runin block "$R" 'GIT_DIR=/x/.git git commit -m x'
|
|
runin block "$R" 'env GIT_WORK_TREE=/x git commit -m x'
|
|
runin block "$R" 'git --git-dir=/x/.git commit -m x'
|
|
runin block "$R" 'git -C /tmp commit -m x'
|
|
runin block "$R" 'git -C $(mktemp -d) commit -m x'
|
|
runin block "$tmp" "cd $R && cd /tmp && git commit -m x"
|
|
runin block "$R" 'cd - && git commit -m x'
|
|
runin block "$R" 'cd $HOME && git commit -m x'
|
|
runin block "$R" 'pushd /tmp && git commit -m x'
|
|
runin block "$tmp" 'git commit -m x'
|
|
runin block "$tmp/other" 'git commit -m x'
|
|
runin block "$tmp" "git -C $tmp/other merge x"
|
|
echo "--- fail-open ---"
|
|
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"
|
|
[ $? -eq 0 ] && echo "ok non-Bash tool ignored" || {
|
|
echo "MISMATCH non-Bash"
|
|
fail=1
|
|
}
|
|
printf 'not json' | python3 "$G"
|
|
[ $? -eq 0 ] && echo "ok garbage stdin passes" || {
|
|
echo "MISMATCH garbage"
|
|
fail=1
|
|
}
|
|
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || {
|
|
echo "GIT-GUARD MATRIX: mismatches above"
|
|
exit 1
|
|
}
|