| Euan Kemp | aabca2e | 2025-07-21 05:44:44 +0000 | [diff] [blame] | 1 | #!/usr/bin/env bash |
| Philip Zeyliger | 254c49f | 2025-07-17 17:26:24 -0700 | [diff] [blame] | 2 | # Pre-receive hook for sketch git http server |
| 3 | # Handles refs/remotes/origin/Y pushes by forwarding them to origin/Y |
| 4 | |
| 5 | set -e |
| 6 | |
| 7 | # Timeout function for commands (macOS compatible) |
| 8 | # Usage: run_with_timeout <timeout_seconds> <command> [args...] |
| 9 | # |
| 10 | # This is here because OX X doesn't ship /usr/bin/timeout by default!?!? |
| 11 | run_with_timeout() { |
| 12 | local timeout=$1 |
| 13 | shift |
| 14 | |
| 15 | # Run command in background and capture PID |
| 16 | "$@" & |
| 17 | local cmd_pid=$! |
| 18 | |
| 19 | # Start timeout killer in background |
| 20 | ( |
| 21 | sleep "$timeout" |
| 22 | if kill -0 "$cmd_pid" 2>/dev/null; then |
| 23 | echo "Command timed out after ${timeout}s, killing process" >&2 |
| 24 | kill -TERM "$cmd_pid" 2>/dev/null || true |
| 25 | sleep 2 |
| 26 | kill -KILL "$cmd_pid" 2>/dev/null || true |
| 27 | fi |
| 28 | ) & |
| 29 | local killer_pid=$! |
| 30 | |
| 31 | # Wait for command to complete |
| 32 | local exit_code=0 |
| 33 | if wait "$cmd_pid" 2>/dev/null; then |
| 34 | exit_code=$? |
| 35 | else |
| 36 | exit_code=124 # timeout exit code |
| 37 | fi |
| 38 | |
| 39 | # Clean up timeout killer |
| 40 | kill "$killer_pid" 2>/dev/null || true |
| 41 | wait "$killer_pid" 2>/dev/null || true |
| 42 | |
| 43 | return $exit_code |
| 44 | } |
| 45 | |
| 46 | # Read stdin for ref updates |
| 47 | while read oldrev newrev refname; do |
| 48 | # Check if this is a push to refs/remotes/origin/Y pattern |
| 49 | if [[ "$refname" =~ ^refs/remotes/origin/(.+)$ ]]; then |
| 50 | branch_name="${BASH_REMATCH[1]}" |
| 51 | |
| 52 | # Check if this is a force push by seeing if oldrev is not ancestor of newrev |
| 53 | if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then |
| 54 | # Check if this is a fast-forward (oldrev is ancestor of newrev) |
| 55 | if ! git merge-base --is-ancestor "$oldrev" "$newrev" 2>/dev/null; then |
| 56 | echo "Error: Force push detected to refs/remotes/origin/$branch_name" >&2 |
| 57 | echo "Force pushes are not allowed" >&2 |
| 58 | exit 1 |
| 59 | fi |
| 60 | fi |
| 61 | |
| 62 | echo "Detected push to refs/remotes/origin/$branch_name" >&2 |
| 63 | |
| 64 | # Verify HTTP_USER_AGENT is set to sketch-intentional-push for forwarding |
| 65 | if [ "$HTTP_USER_AGENT" != "sketch-intentional-push" ]; then |
| 66 | echo "Error: Unauthorized push to refs/remotes/origin/$branch_name" >&2 |
| 67 | exit 1 |
| 68 | fi |
| 69 | |
| 70 | echo "Authorization verified, forwarding to origin" >&2 |
| 71 | |
| 72 | # Push to origin using the new commit with 10 second timeout |
| 73 | # There's an innocous "ref updates forbidden inside quarantine environment" warning that we can ignore. |
| 74 | if ! run_with_timeout 10 git push origin "$newrev:refs/heads/$branch_name"; then |
| 75 | echo "Error: Failed to push $newrev to origin/$branch_name (may have timed out)" >&2 |
| 76 | exit 1 |
| 77 | fi |
| 78 | |
| 79 | echo "Successfully pushed to origin/$branch_name" >&2 |
| 80 | fi |
| 81 | done |
| 82 | |
| 83 | exit 0 |