#!/usr/bin/env bash # git-deploy -m "message" # # Depuis la branche de dev : commit (message obligatoire s'il y a des modifs) # → merge dans une ou plusieurs branches d'env → push (dernière étape, atomique) # → retour dev. # # Sécurité : toutes les mutations locales sont faites AVANT tout push. # En cas d'échec local, l'arbre est restauré à l'identique 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 deploy -m "message"'; } 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]:-}" BRANCHES=("${POS[@]:1}") { [ -n "$REMOTE" ] && [ "${#BRANCHES[@]}" -ge 1 ]; } || { usage; exit 1; } # ---- Validations pré-vol (aucune mutation) --------------------------------- git remote get-url "$REMOTE" >/dev/null 2>&1 || die "Remote inconnu : $REMOTE" seen="" for b in "${BRANCHES[@]}"; do git show-ref --verify --quiet "refs/heads/$b" || die "Branche locale inconnue : $b" [ "$b" != "$SOURCE" ] || die "'$SOURCE' est la branche de dev, pas une branche de déploiement." case " $seen " in *" $b "*) die "Branche indiquée en double : $b" ;; esac seen="$seen $b" done current=$(git branch --show-current) [ "$current" = "$SOURCE" ] || die "Place-toi sur '$SOURCE' pour déployer (git switch $SOURCE)." assert_clean_git_state dirty=0; [ -n "$(git status --porcelain)" ] && dirty=1 if [ "$dirty" = 1 ]; then { [ "$MSG_SET" = 1 ] && [ -n "$MSG" ]; } || die 'Des modifs sont présentes : message requis (-m "…").' fi # ---- Confirmation ---------------------------------------------------------- list="${BRANCHES[*]}" if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ($list, atomique)"; else push_line="NON (désactivé)"; fi [ "$dirty" = 1 ] && commit_line="commit sur '$SOURCE' : \"$MSG\"" || commit_line="aucun commit (arbre propre)" confirm " • $commit_line • merge '$SOURCE' → $list • push : $push_line" # ---- Sauvegarde de l'état pour rollback ------------------------------------ SAVED_SOURCE=$(git rev-parse --verify "refs/heads/$SOURCE") SAVED_BRANCH=() for b in "${BRANCHES[@]}"; do SAVED_BRANCH+=("$(git rev-parse --verify "refs/heads/$b")"); done COMMITTED=0 rollback() { set +e echo "↩ Restauration de l'état initial…" >&2 local gd i; gd=$(git rev-parse --git-dir) [ -e "$gd/MERGE_HEAD" ] && git merge --abort git reset --hard -q for i in "${!BRANCHES[@]}"; do git update-ref "refs/heads/${BRANCHES[$i]}" "${SAVED_BRANCH[$i]}" done git switch -q "$SOURCE" 2>/dev/null || git switch -q --force "$SOURCE" if [ "$COMMITTED" = 1 ]; then # défait le commit mais garde les fichiers modifiés (état d'origine) git reset --mixed -q "$SAVED_SOURCE" else git reset --hard -q "$SAVED_SOURCE" fi echo "✓ État initial restauré. Aucun push effectué." >&2 } fail() { echo "✗ $1" >&2; rollback; exit 1; } # ---- Mutations locales (aucun réseau) -------------------------------------- if [ "$dirty" = 1 ]; then git add -A git commit -q -m "$MSG" || fail "Échec du commit sur '$SOURCE'." COMMITTED=1 echo "✓ Commit sur '$SOURCE' : $MSG" fi for b in "${BRANCHES[@]}"; do git switch -q "$b" || fail "Impossible de basculer sur '$b'." git merge --no-edit -q "$SOURCE" || fail "Conflit de merge sur '$b'." echo "✓ '$b' à jour depuis '$SOURCE'." done git switch -q "$SOURCE" || fail "Impossible de revenir sur '$SOURCE'." echo "✓ Local OK : $list." # ---- Push : dernière étape, atomique, jamais forcé ------------------------- if [ "$PUSH" = true ]; then echo "→ Vérification du push (dry-run)…" git push --atomic --dry-run "$REMOTE" "${BRANCHES[@]}" \ || { echo "✗ Push rejeté au dry-run (non fast-forward ?). RIEN poussé. Local conservé." >&2; exit 1; } echo "→ Push atomique ($list) → '$REMOTE'…" git push --atomic "$REMOTE" "${BRANCHES[@]}" \ || { echo "✗ Échec du push. RIEN poussé (atomique). Local conservé." >&2; \ echo " Relance : git push --atomic $REMOTE $list" >&2; exit 1; } echo "✓ Poussé sur '$REMOTE'." else echo "→ (push désactivé) aurait fait : git push --atomic $REMOTE $list" fi echo "✓ Terminé. Tu es sur '$SOURCE'."