diff --git a/README.md b/README.md index c8fb9e3..cf29908 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ une branche de développement, des branches d'environnement, et deux sous-commandes git **transactionnelles** et **sûres**. ```bash -git deploy -m "message" # livrer le dev vers un environnement -git hotfix -m "message" # corriger un environnement puis rapatrier dans le dev +git deploy -m "message" # livrer le dev vers un ou plusieurs environnements +git hotfix -m "message" # corriger un environnement puis rapatrier dans le dev ``` ## Modèle de branches @@ -72,6 +72,13 @@ git deploy gitea prod -m "ajout du bloc contact" 3. push atomique (dernière étape) ; 4. retour sur `master`. +Plusieurs environnements en une commande (commit unique, merges enchaînés, +**push atomique de tout à la fin** — tout ou rien) : + +```bash +git deploy gitea prod stage -m "ajout du bloc contact" +``` + ### Corriger un environnement (hotfix) ```bash diff --git a/deploy-completion.bash b/deploy-completion.bash index a04eb03..9182dac 100644 --- a/deploy-completion.bash +++ b/deploy-completion.bash @@ -1,12 +1,14 @@ # Complétion bash pour « git deploy » et « git hotfix ». -# Complète le 1er argument avec les remotes, le 2e avec les branches locales, -# le tout lu en direct depuis le dépôt courant. +# git deploy : 1er arg = remote, args suivants = branches (multi-branches). +# git hotfix : 1er arg = remote, 2e arg = branche. +# Tout est lu en direct depuis le dépôt courant. # # À sourcer dans ~/.bashrc APRÈS la complétion git, par ex. : -# source /chemin/vers/src/bin/deploy-completion.bash +# source /chemin/vers/gitploy/deploy-completion.bash -_deploy_complete_positional() { - local i args=() cur="${COMP_WORDS[COMP_CWORD]}" +# $1 = "multi" pour autoriser plusieurs branches, sinon une seule. +_deploy_complete() { + local multi="$1" i args=() cur="${COMP_WORDS[COMP_CWORD]}" # Recense les arguments positionnels déjà saisis (hors options et valeur de -m) for ((i = 2; i < COMP_CWORD; i++)); do case "${COMP_WORDS[i-1]}" in -m|--message) continue ;; esac @@ -14,13 +16,25 @@ _deploy_complete_positional() { args+=("${COMP_WORDS[i]}") done - case "${#args[@]}" in - 0) COMPREPLY=($(compgen -W "$(git remote 2>/dev/null)" -- "$cur")) ;; - 1) COMPREPLY=($(compgen -W "$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null)" -- "$cur")) ;; - *) COMPREPLY=() ;; - esac + local n=${#args[@]} + if [ "$n" -eq 0 ]; then + COMPREPLY=($(compgen -W "$(git remote 2>/dev/null)" -- "$cur")) + return + fi + if [ "$multi" != multi ] && [ "$n" -ge 2 ]; then + COMPREPLY=(); return + fi + + # Branches locales, en excluant celles déjà saisies. + local all excl=" ${args[*]:1} " + all=$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null) + local avail="" + for b in $all; do + case "$excl" in *" $b "*) ;; *) avail="$avail $b" ;; esac + done + COMPREPLY=($(compgen -W "$avail" -- "$cur")) } # Hooks reconnus par la complétion git pour les sous-commandes personnalisées. -_git_deploy() { _deploy_complete_positional; } -_git_hotfix() { _deploy_complete_positional; } +_git_deploy() { _deploy_complete multi; } +_git_hotfix() { _deploy_complete single; } diff --git a/git-deploy b/git-deploy index 13bf4a5..da98d1c 100755 --- a/git-deploy +++ b/git-deploy @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# git-deploy -m "message" +# git-deploy -m "message" # # Depuis la branche de dev : commit (message obligatoire s'il y a des modifs) -# → merge dans la branche d'env → push (dernière étape, atomique) → retour dev. +# → 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é. @@ -16,7 +17,7 @@ DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck source=_deploy_lib.sh . "$DIR/_deploy_lib.sh" -usage() { echo 'Usage : git deploy -m "message"'; } +usage() { echo 'Usage : git deploy -m "message"'; } SOURCE=$(deploy_source) PUSH=$(deploy_push) @@ -30,13 +31,19 @@ while [ $# -gt 0 ]; do *) POS+=("$1"); shift ;; esac done -REMOTE="${POS[0]:-}"; BRANCH="${POS[1]:-}" -{ [ -n "$REMOTE" ] && [ -n "$BRANCH" ]; } || { usage; exit 1; } +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" -git show-ref --verify --quiet "refs/heads/$BRANCH" || die "Branche locale inconnue : $BRANCH" -[ "$BRANCH" != "$SOURCE" ] || die "'$SOURCE' est la branche de dev, pas une branche de déploiement." +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 @@ -47,24 +54,28 @@ if [ "$dirty" = 1 ]; then fi # ---- Confirmation ---------------------------------------------------------- -if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ('$BRANCH' + '$SOURCE', atomique)"; else push_line="NON (désactivé)"; fi +list="${BRANCHES[*]}" +if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ($list + '$SOURCE', 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' → '$BRANCH' + • merge '$SOURCE' → $list • 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") +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; gd=$(git rev-parse --git-dir) + local gd i; gd=$(git rev-parse --git-dir) [ -e "$gd/MERGE_HEAD" ] && git merge --abort git reset --hard -q - git update-ref "refs/heads/$BRANCH" "$SAVED_BRANCH" + 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) @@ -84,22 +95,25 @@ if [ "$dirty" = 1 ]; then echo "✓ Commit sur '$SOURCE' : $MSG" fi -git switch -q "$BRANCH" || fail "Impossible de basculer sur '$BRANCH'." -git merge --no-edit -q "$SOURCE" || fail "Conflit de merge sur '$BRANCH'." +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 : '$BRANCH' à jour depuis '$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" "$BRANCH" "$SOURCE" \ + git push --atomic --dry-run "$REMOTE" "${BRANCHES[@]}" "$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 "→ Push atomique ($list + '$SOURCE') → '$REMOTE'…" + git push --atomic "$REMOTE" "${BRANCHES[@]}" "$SOURCE" \ || { echo "✗ Échec du push. RIEN poussé (atomique). Local conservé." >&2; \ - echo " Relance : git push --atomic $REMOTE $BRANCH $SOURCE" >&2; exit 1; } + echo " Relance : git push --atomic $REMOTE $list $SOURCE" >&2; exit 1; } echo "✓ Poussé sur '$REMOTE'." else - echo "→ (push désactivé) aurait fait : git push --atomic $REMOTE $BRANCH $SOURCE" + echo "→ (push désactivé) aurait fait : git push --atomic $REMOTE $list $SOURCE" fi echo "✓ Terminé. Tu es sur '$SOURCE'."