#!/usr/bin/env bash # `git cp` wrapper — the real pre-apply guard the hooks can't be. # # git cp ... pre-check ledger, cherry-pick -x, auto-reconcile # git cp --force ... pick even known "Won't merge" SHAs (documented override) # git cp --continue|--abort|--skip|--quit forwarded to git cherry-pick # # Behaviour: # 1. Resolve requested SHAs (single, list, or A^..B range) BEFORE touching the tree. # 2. If any is disposition "wont-merge", print SHA + reason and ABORT unless --force. # 3. git cherry-pick -x (ensures -x is present exactly once). # 4. On success, run `scripts/triage.py reconcile` to land the picks in the ledger. set -uo pipefail root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "git cp: not a git repository" >&2 exit 1 } triage="$root/scripts/triage.py" py="$(command -v python3 || command -v python 2>/dev/null)" force=0 control=0 passthru=() picks=() for arg in "$@"; do case "$arg" in --force | --allow-reject) force=1 ;; --continue | --abort | --skip | --quit) control=1 passthru+=("$arg") ;; -*) passthru+=("$arg") ;; *) passthru+=("$arg") picks+=("$arg") ;; esac done reconcile() { if [ -n "$py" ] && [ -f "$triage" ]; then "$py" "$triage" reconcile else echo "git cp: python/triage.py unavailable — skipping auto-reconcile" >&2 fi } ensure_x() { # Prepend -x unless the caller already passed it. for a in "${passthru[@]}"; do [ "$a" = "-x" ] && return 0 done passthru=("-x" "${passthru[@]}") } # Sequencer control verbs: forward and (for --continue) reconcile whatever landed. if [ "$control" -eq 1 ]; then git cherry-pick "${passthru[@]}" rc=$? if [ "$rc" -eq 0 ]; then reconcile fi exit "$rc" fi # Pre-apply reject check. if [ -n "$py" ] && [ -f "$triage" ] && [ "${#picks[@]}" -gt 0 ]; then resolved=() for tok in "${picks[@]}"; do if [[ "$tok" == *..* ]]; then while IFS= read -r s; do [ -n "$s" ] && resolved+=("$s") done < <(git rev-list --reverse "$tok" 2>/dev/null) else s="$(git rev-parse --verify --quiet "${tok}^{commit}" 2>/dev/null)" && resolved+=("$s") fi done rejects=() for s in ${resolved[@]+"${resolved[@]}"}; do reason="$("$py" "$triage" check-reject "$s" 2>/dev/null)" if [ "$?" -eq 3 ]; then rejects+=("${s:0:12} $reason") fi done if [ "${#rejects[@]}" -gt 0 ]; then echo "git cp: the following SHA(s) are marked \"Won't merge\" in the triage ledger:" >&2 for r in "${rejects[@]}"; do echo " ⛔ $r" >&2 done if [ "$force" -eq 0 ]; then echo "" >&2 echo "Refusing to cherry-pick a known-reject. To override intentionally: git cp --force ..." >&2 exit 1 fi echo " --force set — proceeding anyway." >&2 fi fi ensure_x # --force must also satisfy the commit-msg backstop, so allow rejects through the hook too. if [ "$force" -eq 1 ]; then export SH_CHERRYPICK_ALLOW_REJECT=1 fi git cherry-pick "${passthru[@]}" rc=$? if [ "$rc" -eq 0 ]; then reconcile else echo "" >&2 echo "git cp: cherry-pick stopped (rc=$rc). Resolve, then: git cp --continue (or --abort/--skip)." >&2 echo " Landed picks are journaled; reconcile runs on --continue or via make triage-reconcile." >&2 fi exit "$rc"