|
#!/usr/bin/env bash |
|
# ------------------------------------------------------------------- |
|
# Git Pairing Setup |
|
# |
|
# Installs a global prepare-commit-msg hook and Git aliases to make |
|
# pair programming commits painless. Automatically appends |
|
# `Co-authored-by` trailers to every commit while pairing. |
|
# |
|
# π¦ Install: |
|
# curl -fsSL https://gist.githubusercontent.com/oboxodo/9d086bd582ff3df822b17a3396fb5e51/raw/git-pairing-setup.sh | bash |
|
# # add --yes for non-interactive uninstall |
|
# |
|
# ποΈ Uninstall: |
|
# curl -fsSL https://gist.githubusercontent.com/oboxodo/9d086bd582ff3df822b17a3396fb5e51/raw/git-pairing-setup.sh | bash -s -- --uninstall |
|
# # add --yes for non-interactive uninstall |
|
# |
|
# π» Usage after install: |
|
# git pair # show current co-authors |
|
# git pair "Name" [email protected] # add a co-author (append mode) |
|
# git solo # clear co-authors |
|
# git uninstall-pairing-hook # remove hook & aliases |
|
# |
|
# β οΈ Disclaimer & License: |
|
# This script was vive-coded with ChatGPT 5. |
|
# Provided "AS IS", without warranty of any kind. Use at your own risk. |
|
# MIT License Β© 2025 Diego Algorta |
|
# ------------------------------------------------------------------- |
|
|
|
set -euo pipefail |
|
|
|
PAIR_DIR="${HOME}/.local/share/git-pair" |
|
DEFAULT_HOOK_DIR="${HOME}/.config/git/hooks" |
|
COAUTHOR_FILE="${HOME}/.git-coauthor" |
|
|
|
FLAG_UNINSTALL=0 |
|
FLAG_YES=0 |
|
|
|
# Parse flags (works with curl|bash via "-s -- <args>") |
|
while [ "${1-}" ]; do |
|
case "$1" in |
|
--uninstall) FLAG_UNINSTALL=1 ;; |
|
--yes|-y) FLAG_YES=1 ;; |
|
*) echo "Unknown option: $1" >&2; exit 2 ;; |
|
esac |
|
shift || true |
|
done |
|
|
|
# Confirmation that works when stdin is piped (reads from /dev/tty) |
|
confirm() { |
|
if [ "$FLAG_YES" -eq 1 ]; then |
|
return 0 |
|
fi |
|
if [ -t 0 ]; then |
|
read -r -p "Proceed? [y/N] " ans |
|
elif [ -r /dev/tty ]; then |
|
read -r -p "Proceed? [y/N] " ans </dev/tty |
|
else |
|
echo "Non-interactive run. Re-run with --yes to confirm." >&2 |
|
exit 1 |
|
fi |
|
case "${ans:-}" in y|Y) return 0 ;; *) echo "Aborted."; exit 1 ;; esac |
|
} |
|
|
|
current_hooks_path() { |
|
git config --global --get core.hooksPath 2>/dev/null || true |
|
} |
|
|
|
install_all() { |
|
# Decide where to place the hook: |
|
local cur hooks_dir |
|
cur="$(current_hooks_path)" |
|
if [ -n "${cur}" ]; then |
|
hooks_dir="${cur}" |
|
echo "βΉοΈ Detected existing core.hooksPath: ${hooks_dir}" |
|
echo " We'll use that and NOT modify core.hooksPath." |
|
else |
|
hooks_dir="${DEFAULT_HOOK_DIR}" |
|
fi |
|
local hook_file="${hooks_dir}/prepare-commit-msg" |
|
|
|
echo "π§ This will:" |
|
echo " β’ Install a global git hook at: ${hook_file}" |
|
[ -z "${cur}" ] && echo " β’ Set core.hooksPath to: ${hooks_dir}" |
|
echo " β’ Set git aliases: install-pairing-hook, uninstall-pairing-hook, pair, solo" |
|
confirm |
|
|
|
mkdir -p "${PAIR_DIR}" "${hooks_dir}" |
|
|
|
# --- Helper: install-hook --- |
|
cat > "${PAIR_DIR}/install-hook.sh" <<'SH' |
|
#!/usr/bin/env sh |
|
set -eu |
|
DEFAULT_HOOK_DIR="${HOME}/.config/git/hooks" |
|
CURR="$(git config --global --get core.hooksPath 2>/dev/null || true)" |
|
HOOK_DIR="${CURR:-$DEFAULT_HOOK_DIR}" |
|
HOOK_FILE="${HOOK_DIR}/prepare-commit-msg" |
|
|
|
mkdir -p "${HOOK_DIR}" |
|
# The hook itself (no expansions inside heredoc) |
|
cat > "${HOOK_FILE}" <<'EOF' |
|
#!/usr/bin/env sh |
|
# Appends Co-authored-by trailers from ~/.git-coauthor if present. |
|
# Git passes the commit message filepath as $1. |
|
set -eu |
|
COAUTHOR_FILE="${HOME}/.git-coauthor" |
|
[ -f "${COAUTHOR_FILE}" ] || exit 0 |
|
|
|
# Avoid adding if a Co-authored-by trailer already exists. |
|
if ! grep -q "^Co-authored-by:" "$1"; then |
|
printf "\n" >> "$1" |
|
# Append exactly as-is; each line must be a proper trailer. |
|
cat "${COAUTHOR_FILE}" >> "$1" |
|
fi |
|
EOF |
|
chmod 0755 "${HOOK_FILE}" |
|
|
|
# Only set core.hooksPath if it was empty |
|
if [ -z "${CURR}" ]; then |
|
git config --global core.hooksPath "${HOOK_DIR}" |
|
echo "β
Hook installed at ${HOOK_FILE}; core.hooksPath set to ${HOOK_DIR}" |
|
else |
|
echo "β
Hook installed at ${HOOK_FILE}; core.hooksPath unchanged (${CURR})" |
|
fi |
|
SH |
|
chmod 0755 "${PAIR_DIR}/install-hook.sh" |
|
|
|
# --- Helper: uninstall-hook --- |
|
cat > "${PAIR_DIR}/uninstall-hook.sh" <<'SH' |
|
#!/usr/bin/env sh |
|
set -eu |
|
DEFAULT_HOOK_DIR="${HOME}/.config/git/hooks" |
|
CURR="$(git config --global --get core.hooksPath 2>/dev/null || true)" |
|
HOOK_DIR="${CURR:-$DEFAULT_HOOK_DIR}" |
|
HOOK_FILE="${HOOK_DIR}/prepare-commit-msg" |
|
|
|
# Remove only our hook file, leave the directory alone. |
|
if [ -f "${HOOK_FILE}" ]; then |
|
rm -f "${HOOK_FILE}" |
|
echo "ποΈ removed ${HOOK_FILE}" |
|
else |
|
echo "βΉοΈ no hook file at ${HOOK_FILE}" |
|
fi |
|
|
|
# Only unset hooksPath if we set it (i.e., it equals DEFAULT_HOOK_DIR) |
|
if [ -n "${CURR}" ] && [ "${CURR}" = "${DEFAULT_HOOK_DIR}" ]; then |
|
git config --global --unset core.hooksPath || true |
|
echo "π§Ή unset core.hooksPath (was ${DEFAULT_HOOK_DIR})" |
|
fi |
|
SH |
|
chmod 0755 "${PAIR_DIR}/uninstall-hook.sh" |
|
|
|
# --- Helper: pair (append co-authors & show list when no args) --- |
|
cat > "${PAIR_DIR}/pair.sh" <<'SH' |
|
#!/usr/bin/env sh |
|
set -eu |
|
CURR="$(git config --global --get core.hooksPath 2>/dev/null || true)" |
|
HOOK_DIR="${CURR:-$HOME/.config/git/hooks}" |
|
HOOK_FILE="${HOOK_DIR}/prepare-commit-msg" |
|
COAUTHOR_FILE="${HOME}/.git-coauthor" |
|
|
|
if [ ! -x "${HOOK_FILE}" ]; then |
|
echo "β pairing hook not found at ${HOOK_FILE}" |
|
echo "β‘οΈ Run: git install-pairing-hook" |
|
exit 1 |
|
fi |
|
|
|
# If no args β just show the current list |
|
if [ "$#" -eq 0 ]; then |
|
if [ -s "${COAUTHOR_FILE}" ]; then |
|
echo "π₯ Current co-authors:" |
|
cat "${COAUTHOR_FILE}" |
|
else |
|
echo "π€ No co-authors set (solo mode)." |
|
fi |
|
exit 0 |
|
fi |
|
|
|
if [ "$#" -lt 2 ]; then |
|
echo "Usage: git pair \"Name\" [email protected]" |
|
exit 2 |
|
fi |
|
|
|
NAME="$1" |
|
EMAIL="$2" |
|
|
|
case "${NAME}" in *$'\n'*|*'<'*|*'>'* ) echo "β Invalid name"; exit 2;; esac |
|
case "${EMAIL}" in *$'\n'*|*'<'*|*'>'* ) echo "β Invalid email"; exit 2;; esac |
|
echo "${EMAIL}" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$' || { echo "β Invalid email format"; exit 2; } |
|
|
|
LINE=$(printf "Co-authored-by: %s <%s>" "${NAME}" "${EMAIL}") |
|
|
|
[ -f "${COAUTHOR_FILE}" ] || : > "${COAUTHOR_FILE}" |
|
|
|
if grep -Fxq "${LINE}" "${COAUTHOR_FILE}"; then |
|
echo "β©οΈ Co-author already present:" |
|
else |
|
printf "%s\n" "${LINE}" >> "${COAUTHOR_FILE}" |
|
echo "β Added co-author:" |
|
fi |
|
|
|
echo " ${LINE}" |
|
echo |
|
echo "π₯ Current co-authors:" |
|
cat "${COAUTHOR_FILE}" |
|
SH |
|
chmod 0755 "${PAIR_DIR}/pair.sh" |
|
|
|
# --- Helper: solo (clear co-authors) --- |
|
cat > "${PAIR_DIR}/solo.sh" <<'SH' |
|
#!/usr/bin/env sh |
|
set -eu |
|
COAUTHOR_FILE="${HOME}/.git-coauthor" |
|
if [ -f "${COAUTHOR_FILE}" ]; then |
|
rm -f "${COAUTHOR_FILE}" |
|
echo "π§βπ» Solo mode (co-authors cleared)" |
|
else |
|
echo "βΉοΈ Already in solo mode" |
|
fi |
|
SH |
|
chmod 0755 "${PAIR_DIR}/solo.sh" |
|
|
|
# Aliases (reset to be safe) |
|
for a in pair solo install-pairing-hook uninstall-pairing-hook; do |
|
git config --global --unset alias."$a" 2>/dev/null || true |
|
done |
|
git config --global alias.install-pairing-hook "!${PAIR_DIR}/install-hook.sh" |
|
git config --global alias.uninstall-pairing-hook "!${PAIR_DIR}/uninstall-hook.sh" |
|
git config --global alias.pair "!${PAIR_DIR}/pair.sh" |
|
git config --global alias.solo "!${PAIR_DIR}/solo.sh" |
|
|
|
# Install the hook now so it's ready |
|
"${PAIR_DIR}/install-hook.sh" |
|
|
|
echo |
|
echo "π Done." |
|
echo "Usage:" |
|
echo " git pair # show current co-authors" |
|
echo " git pair \"Name\" [email protected] # add a co-author" |
|
echo " git solo # clear co-authors" |
|
echo " git uninstall-pairing-hook" |
|
} |
|
|
|
uninstall_all() { |
|
echo "β οΈ This will remove the hook, aliases, ${PAIR_DIR}, and ${COAUTHOR_FILE}." |
|
confirm |
|
for a in pair solo install-pairing-hook uninstall-pairing-hook; do |
|
git config --global --unset alias."$a" 2>/dev/null || true |
|
done |
|
"${PAIR_DIR}/uninstall-hook.sh" 2>/dev/null || true |
|
rm -rf "${PAIR_DIR}" |
|
rm -f "${COAUTHOR_FILE}" |
|
echo "β
Uninstalled." |
|
} |
|
|
|
if [ "$FLAG_UNINSTALL" -eq 1 ]; then |
|
uninstall_all |
|
else |
|
install_all |
|
fi |