#!/usr/bin/env bash # git-hotfix -m "message" # # Sans changer de branche à la main. Tu es sur la branche de dev avec un # correctif en cours (non commité). Le script : # 1. met tes modifs de côté (stash), # 2. bascule sur la branche d'env et y applique le correctif, # 3. le committe (message obligatoire), # 4. le rapatrie (merge) dans la branche de dev, # 5. push en dernière étape (atomique) puis te ramène sur la branche de dev. # # Sécurité : toutes les mutations locales sont faites AVANT tout push. # En cas d'échec local, l'arbre est restauré à l'identique (tes modifs # reviennent en l'état sur la branche de dev) et rien n'est poussé. # # Config git-native : # git config deploy.source (défaut : master) # git config deploy.push true|false (défaut : false — push désactivé) set -euo pipefail DIR=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd) # shellcheck source=_deploy_lib.sh . "$DIR/_deploy_lib.sh" usage() { echo 'Usage : git hotfix -m "message" (depuis la branche de dev)'; } SOURCE=$(deploy_source) PUSH=$(deploy_push) MSG=""; MSG_SET=0; POS=() while [ $# -gt 0 ]; do case "$1" in -m|--message) MSG="${2:-}"; MSG_SET=1; shift 2 ;; -h|--help) usage; exit 0 ;; -*) usage; die "Option inconnue : $1" ;; *) POS+=("$1"); shift ;; esac done REMOTE="${POS[0]:-}"; BRANCH="${POS[1]:-}" { [ -n "$REMOTE" ] && [ -n "$BRANCH" ]; } || { usage; exit 1; } # ---- Validations pré-vol (aucune mutation) --------------------------------- git remote get-url "$REMOTE" >/dev/null 2>&1 || die "Remote inconnu : $REMOTE" git show-ref --verify --quiet "refs/heads/$BRANCH" || die "Branche locale inconnue : $BRANCH" [ "$BRANCH" != "$SOURCE" ] || die "'$SOURCE' est la branche de dev, pas une cible de hotfix." current=$(git branch --show-current) [ "$current" = "$SOURCE" ] || die "Lance 'git hotfix' depuis la branche de dev '$SOURCE' (git switch $SOURCE)." assert_clean_git_state [ -n "$(git status --porcelain)" ] || die "Aucune modification à hotfixer sur '$SOURCE'." { [ "$MSG_SET" = 1 ] && [ -n "$MSG" ]; } || die 'Message requis : -m "…".' # ---- Confirmation ---------------------------------------------------------- if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ('$BRANCH' + '$SOURCE', atomique)"; else push_line="NON (désactivé)"; fi confirm " • stash des modifs de '$SOURCE' • application + commit sur '$BRANCH' : \"$MSG\" • rapatriement (merge) dans '$SOURCE' • push : $push_line" # ---- Sauvegarde de l'état pour rollback ------------------------------------ SAVED_SOURCE=$(git rev-parse --verify "refs/heads/$SOURCE") SAVED_BRANCH=$(git rev-parse --verify "refs/heads/$BRANCH") STASH_MADE=0 rollback() { set +e echo "↩ Restauration de l'état initial…" >&2 local gd; gd=$(git rev-parse --git-dir) [ -e "$gd/MERGE_HEAD" ] && git merge --abort git reset --hard -q git update-ref "refs/heads/$SOURCE" "$SAVED_SOURCE" git update-ref "refs/heads/$BRANCH" "$SAVED_BRANCH" git switch -q "$SOURCE" 2>/dev/null || git switch -q --force "$SOURCE" git reset --hard -q "$SAVED_SOURCE" if [ "$STASH_MADE" = 1 ]; then # le stash est conservé tant qu'on ne l'a pas 'drop' : on le remet en place git stash pop -q 2>/dev/null \ || echo "⚠ Tes modifs sont sauvegardées dans le stash (git stash list)." >&2 fi echo "✓ État initial restauré. Aucun push effectué." >&2 } fail() { echo "✗ $1" >&2; rollback; exit 1; } # ---- Mutations locales (aucun réseau) -------------------------------------- git stash push --include-untracked -q -m "git-hotfix: $MSG" || fail "Échec du stash." STASH_MADE=1 git switch -q "$BRANCH" || fail "Impossible de basculer sur '$BRANCH'." # 'apply' (et non 'pop') : le stash reste dispo pour un rollback propre en cas de conflit. git stash apply -q || fail "Conflit en appliquant le correctif sur '$BRANCH'." git add -A git commit -q -m "$MSG" || fail "Échec du commit sur '$BRANCH'." git switch -q "$SOURCE" || fail "Impossible de revenir sur '$SOURCE'." git merge --no-edit -q "$BRANCH" || fail "Conflit en rapatriant dans '$SOURCE'." # Local OK : le correctif est commité sur les deux branches → on lâche le stash. git stash drop -q 2>/dev/null || true STASH_MADE=0 echo "✓ Local OK : hotfix sur '$BRANCH' + rapatrié dans '$SOURCE'." # ---- Push : dernière étape, atomique, jamais forcé ------------------------- if [ "$PUSH" = true ]; then echo "→ Vérification du push (dry-run)…" git push --atomic --dry-run "$REMOTE" "$BRANCH" "$SOURCE" \ || { echo "✗ Push rejeté au dry-run (non fast-forward ?). RIEN poussé. Local conservé." >&2; exit 1; } echo "→ Push atomique '$BRANCH' + '$SOURCE' → '$REMOTE'…" git push --atomic "$REMOTE" "$BRANCH" "$SOURCE" \ || { echo "✗ Échec du push. RIEN poussé (atomique). Local conservé." >&2; \ echo " Relance : git push --atomic $REMOTE $BRANCH $SOURCE" >&2; exit 1; } echo "✓ Poussé sur '$REMOTE'." else echo "→ (push désactivé) aurait fait : git push --atomic $REMOTE $BRANCH $SOURCE" fi echo "✓ Terminé. Tu es sur '$SOURCE'."