74 lines
3.2 KiB
Bash
Executable file
74 lines
3.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Refuse to push dev/ changes to a configured upstream remote.
|
|
#
|
|
# Installed at .git/hooks/pre-push (symlink). See dev/tools/install-hooks.sh.
|
|
#
|
|
# Configure by exporting UPSTREAM_BLOCKED_PATTERN to a substring that matches
|
|
# the upstream remote's URL. Example: if your upstream is
|
|
# git@github.com:upstream-org/repo.git, set
|
|
# export UPSTREAM_BLOCKED_PATTERN='upstream-org/repo'
|
|
# in your shell profile or .envrc. If unset, this hook is a no-op.
|
|
#
|
|
# Git invokes this with: $1 = remote name, $2 = remote URL, and on stdin:
|
|
# <local_ref> <local_sha> <remote_ref> <remote_sha>\n ...
|
|
# Reference: git help githooks (search for "pre-push").
|
|
|
|
set -euo pipefail
|
|
|
|
remote_name="${1:-}"
|
|
remote_url="${2:-}"
|
|
|
|
# If no upstream pattern is configured, this guard is a no-op. Greenfield
|
|
# projects without an upstream-leak concern can leave the variable unset.
|
|
if [ -z "${UPSTREAM_BLOCKED_PATTERN:-}" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Only guard pushes whose target remote URL contains the configured pattern.
|
|
case "$remote_url" in
|
|
*"$UPSTREAM_BLOCKED_PATTERN"*) ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
|
|
zero="0000000000000000000000000000000000000000"
|
|
violation=0
|
|
|
|
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
# Skip branch deletions.
|
|
[ "$local_sha" = "$zero" ] && continue
|
|
|
|
# Determine the set of commits being pushed. On existing branches this is
|
|
# deterministic (remote_sha..local_sha). On a first push of this branch to
|
|
# this remote (remote_sha all-zeros), prefer the target remote's default
|
|
# branch tip as the base; if that doesn't exist, fall back to commits not
|
|
# reachable from ANY remote. We enumerate touched paths via `git log
|
|
# --name-only` so transient commits that add then remove dev/ paths are
|
|
# caught — a `git diff` on endpoints alone would miss those.
|
|
if [ "$remote_sha" != "$zero" ]; then
|
|
revs=("${remote_sha}..${local_sha}")
|
|
elif base=$(git merge-base "$local_sha" "refs/remotes/${remote_name}/main" 2>/dev/null); then
|
|
revs=("${base}..${local_sha}")
|
|
elif base=$(git merge-base "$local_sha" "refs/remotes/${remote_name}/master" 2>/dev/null); then
|
|
revs=("${base}..${local_sha}")
|
|
else
|
|
# No remote-tracking ref for this target — fall back to commits not
|
|
# reachable from any remote. If no remotes are fetched, this degrades
|
|
# to "all ancestry of local_sha" which over-reports, but that's the
|
|
# safe direction for a guard.
|
|
revs=("$local_sha" "--not" "--remotes")
|
|
fi
|
|
|
|
# Enumerate every path touched by any commit in the pushed set.
|
|
touched=$(git log --name-only --pretty=format: "${revs[@]}" 2>/dev/null | sort -u)
|
|
|
|
if echo "$touched" | grep -qE '^dev/'; then
|
|
echo "error: refusing to push 'dev/' changes to a remote matching UPSTREAM_BLOCKED_PATTERN" >&2
|
|
echo " remote: $remote_url" >&2
|
|
echo " pattern match: $UPSTREAM_BLOCKED_PATTERN" >&2
|
|
echo " ref: $local_ref -> $remote_ref" >&2
|
|
echo " dev/ content is excluded from pushes to this upstream. Scope the push, switch remotes, or unset UPSTREAM_BLOCKED_PATTERN if this was unintended." >&2
|
|
violation=1
|
|
fi
|
|
done
|
|
|
|
exit "$violation"
|