Skip to content

Instantly share code, notes, and snippets.

@oboxodo
Last active August 8, 2025 17:58
Show Gist options
  • Save oboxodo/9d086bd582ff3df822b17a3396fb5e51 to your computer and use it in GitHub Desktop.
Save oboxodo/9d086bd582ff3df822b17a3396fb5e51 to your computer and use it in GitHub Desktop.
Simple git pairing helper

Git Pairing Setup

This script installs a global prepare-commit-msg hook and Git aliases to make pair programming commits painless.
While pairing, all commits automatically get Co-authored-by trailers for the configured partners.

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

Notes

  • Should works on macOS & Linux.
  • No shell config changes. Just git aliases.

Disclaimer

This script was vive-coded with ChatGPT 5.
It is provided "AS IS", without warranty of any kind, express or implied.
Use at your own risk. You are solely responsible for any consequences of running it.

License (MIT)

Copyright (c) 2025 Diego Algorta

#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment