Files
RedBear-OS/local/scripts/install-git-hooks.sh
T
vasilito 99e5641127 feat: release-bump pipeline + external graphics version sync
Pipeline (3 operator asks):
- bump-release.sh: canonical orchestrator (forks + sources + external)
- upgrade-forks.sh --to=<tag>: rebase forks with diverged-mode guard
- bump-graphics-recipes.sh: map-driven group-aware graphics bumps
- check-external-versions.sh: drift checker for Qt6/KF6/Plasma/Mesa/Wayland
- refresh-fork-upstream-map.sh: append-only map updater with --check
- post-checkout-version-sync.sh + install-git-hooks.sh: opt-in branch hook
- external_version_lib.py: shared version-parsing/bumping library
- external-upstream-map.toml: ~80 external package entries
- bump-fork.sh: deprecated (REDBEAR_I_KNOW_BUMP_FORK_IS_DEPRECATED=1)
- RELEASE-BUMP-WORKFLOW.md: operator runbook

Quality fixes (8 defects from two independent audits):
- blake2b stable cache keys (was hash(), non-portable)
- atomic cache writes via os.replace
- version_sort_key pre-release demotion (was sorting after finals)
- apply_ver_transform re.error tolerance
- grep || true (pipefail abort)
- cd failure detection in upgrade-forks
- sed URL escape (injection hardening)
- refresh-fork-upstream-map last-row drop fix

Doc cleanup:
- Archive 5 obsolete plans to local/docs/archived/
- Remove 14 stale/superseded docs
- Update 18 docs to reference bump-release.sh and fix inbound links
- TOOLS.md drift fixes
2026-07-18 14:45:41 +09:00

201 lines
7.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-git-hooks.sh — Idempotent installer for Red Bear OS optional git hooks.
#
# Hooks in Red Bear OS are ALWAYS opt-in (per AGENTS.md "absolutely NEVER
# DELETE, NEVER IGNORE" rule — operators explicitly choose to install them).
# This script is a convenience wrapper around the manual `cp` instructions
# documented in local/docs/HOOKS.md. It never silently clobbers an existing
# hook: if a hook file already exists and differs from the canonical copy,
# it is backed up to `<name>.bak` before the new one is written, and a
# warning is printed.
#
# Usage:
# ./local/scripts/install-git-hooks.sh # install post-checkout only (default)
# ./local/scripts/install-git-hooks.sh --all # post-checkout + pre-push + commit-msg
# ./local/scripts/install-git-hooks.sh --uninstall # remove hooks installed by this script
# ./local/scripts/install-git-hooks.sh --uninstall --all
#
# Exit codes:
# 0 success (or already-installed, or nothing to uninstall)
# 1 fatal misconfiguration (not a git repo, scripts missing, etc.)
#
# See: local/docs/HOOKS.md (hook inventory + bypass instructions),
# local/docs/RELEASE-BUMP-WORKFLOW.md (release-bump pipeline).
set -euo pipefail
# ---------------------------------------------------------------------------
# Resolve repo root. Must be a Red Bear OS checkout (has local/scripts/).
# ---------------------------------------------------------------------------
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$ROOT" ]]; then
echo "install-git-hooks: not inside a git work tree" >&2
exit 1
fi
SCRIPTS_DIR="$ROOT/local/scripts"
HOOKS_DIR="$(git rev-parse --git-path hooks 2>/dev/null)"
# `git rev-parse --git-path hooks` resolves correctly for linked worktrees
# and custom $GIT_DIR. Fall back to the conventional path if it fails.
if [[ -z "$HOOKS_DIR" ]]; then
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || echo .git)"
[[ "$GIT_DIR" = /* ]] || GIT_DIR="$ROOT/$GIT_DIR"
HOOKS_DIR="$GIT_DIR/hooks"
fi
mkdir -p "$HOOKS_DIR"
# ---------------------------------------------------------------------------
# Parse flags.
# ---------------------------------------------------------------------------
INSTALL_ALL=0
UNINSTALL=0
for arg in "$@"; do
case "$arg" in
--all) INSTALL_ALL=1 ;;
--uninstall) UNINSTALL=1 ;;
-h|--help)
sed -n '1,30p' "$0"
exit 0
;;
*)
echo "install-git-hooks: unknown option: $arg" >&2
echo " usage: $0 [--all] [--uninstall]" >&2
exit 1
;;
esac
done
# ---------------------------------------------------------------------------
# Hook manifest. The default install set is intentionally minimal (only
# post-checkout) so that a bare `install-git-hooks.sh` does the least
# surprising thing. `--all` extends to the pre-push and commit-msg hooks.
# ---------------------------------------------------------------------------
# Each entry: <hook-name>;<source-script-relative-to-local/scripts>
HOOKS_DEFAULT=(
"post-checkout;post-checkout-version-sync.sh"
)
HOOKS_EXTRA=(
"pre-push;pre-push-checks.sh"
"commit-msg;commit-msg"
)
# Build the active set from the flags.
ACTIVE_HOOKS=()
if [[ $UNINSTALL -eq 1 ]]; then
# Uninstall needs to know the full set that --all would have installed,
# so a default-install-then---all-uninstall cleans everything up.
for entry in "${HOOKS_DEFAULT[@]}" "${HOOKS_EXTRA[@]}"; do
ACTIVE_HOOKS+=("$entry")
done
if [[ $INSTALL_ALL -eq 0 ]]; then
# Without --all, only uninstall the default set (the ones a default
# install would have placed). Operators who used --all should pass
# --uninstall --all.
ACTIVE_HOOKS=("${HOOKS_DEFAULT[@]}")
fi
else
ACTIVE_HOOKS=("${HOOKS_DEFAULT[@]}")
[[ $INSTALL_ALL -eq 1 ]] && ACTIVE_HOOKS+=("${HOOKS_EXTRA[@]}")
fi
# ---------------------------------------------------------------------------
# Install / uninstall loop.
# ---------------------------------------------------------------------------
install_one() {
local hook_name="$1"
local src_rel="$2"
local src="$SCRIPTS_DIR/$src_rel"
local dst="$HOOKS_DIR/$hook_name"
if [[ ! -f "$src" ]]; then
echo "install-git-hooks: WARN source script not found: $src_rel — skipping $hook_name" >&2
return 0
fi
if [[ -e "$dst" || -L "$dst" ]]; then
# Content-based, not path-based: a copied hook has a different path
# than the source, so only byte comparison detects "already current".
if cmp -s "$dst" "$src" 2>/dev/null; then
echo "install-git-hooks: $hook_name already up to date ($dst)"
chmod +x "$dst" 2>/dev/null || true
return 0
fi
# Existing file differs — back it up before overwriting.
local bak="${dst}.bak"
# Rotate any pre-existing .bak so we don't destroy the first backup.
if [[ -e "$bak" ]]; then
local i=1
while [[ -e "${dst}.bak.${i}" && $i -le 100 ]]; do i=$((i+1)); done
if [[ $i -gt 100 ]]; then
bak="${dst}.bak.$(date +%Y%m%d%H%M%S)-$$"
else
bak="${dst}.bak.${i}"
fi
fi
echo "install-git-hooks: WARN existing $hook_name differs — backing up to $bak" >&2
mv "$dst" "$bak"
fi
cp "$src" "$dst"
chmod +x "$dst"
echo "install-git-hooks: installed $hook_name -> $dst"
}
uninstall_one() {
local hook_name="$1"
local dst="$HOOKS_DIR/$hook_name"
if [[ ! -e "$dst" && ! -L "$dst" ]]; then
echo "install-git-hooks: $hook_name not present ($dst) — nothing to remove"
return 0
fi
rm -f "$dst"
echo "install-git-hooks: removed $hook_name ($dst)"
# Restore the most recent backup if one exists, so uninstall is reversible.
if [[ -e "${dst}.bak" ]]; then
mv "${dst}.bak" "$dst"
chmod +x "$dst" 2>/dev/null || true
echo "install-git-hooks: restored previous $hook_name from ${dst}.bak"
fi
}
echo "=== Red Bear OS git hook installer ==="
echo " repo root: $ROOT"
echo " hooks dir: $HOOKS_DIR"
echo " mode: $([[ $UNINSTALL -eq 1 ]] && echo 'uninstall' || echo 'install') ($([[ $INSTALL_ALL -eq 1 ]] && echo 'all hooks' || echo 'default: post-checkout only'))"
echo ""
if [[ $UNINSTALL -eq 1 ]]; then
for entry in "${ACTIVE_HOOKS[@]}"; do
hook_name="${entry%%;*}"
uninstall_one "$hook_name"
done
echo ""
echo "Uninstall complete. Remaining hooks (if any):"
ls -1 "$HOOKS_DIR" 2>/dev/null | grep -vE '\.bak(\.[0-9]+)?$' || echo " (none)"
exit 0
fi
for entry in "${ACTIVE_HOOKS[@]}"; do
hook_name="${entry%%;*}"
src_rel="${entry##*;}"
install_one "$hook_name" "$src_rel"
done
echo ""
echo "=== Install complete ==="
echo ""
echo "Installed hooks are OPT-IN and never auto-commit, auto-push, or block"
echo "checkouts. Bypass / configuration:"
echo " post-checkout: REDBEAR_NO_AUTO_SYNC=1 git checkout <branch>"
echo " pre-push: REDBEAR_SKIP_PRE_PUSH=1 git push (or git push --no-verify)"
echo ""
echo "Full documentation: local/docs/HOOKS.md"
echo "Release-bump runbook: local/docs/RELEASE-BUMP-WORKFLOW.md"