deploy: support multi-branches (git deploy <remote> <branche...>) + complétion

This commit is contained in:
2026-09-07 11:47:15 +02:00
parent d24544db65
commit bc1b09b779
3 changed files with 69 additions and 34 deletions
+8 -1
View File
@@ -5,7 +5,7 @@ une branche de développement, des branches d'environnement, et deux
sous-commandes git **transactionnelles** et **sûres**. sous-commandes git **transactionnelles** et **sûres**.
```bash ```bash
git deploy <remote> <branche> -m "message" # livrer le dev vers un environnement git deploy <remote> <branche...> -m "message" # livrer le dev vers un ou plusieurs environnements
git hotfix <remote> <branche> -m "message" # corriger un environnement puis rapatrier dans le dev git hotfix <remote> <branche> -m "message" # corriger un environnement puis rapatrier dans le dev
``` ```
@@ -72,6 +72,13 @@ git deploy gitea prod -m "ajout du bloc contact"
3. push atomique (dernière étape) ; 3. push atomique (dernière étape) ;
4. retour sur `master`. 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) ### Corriger un environnement (hotfix)
```bash ```bash
+26 -12
View File
@@ -1,12 +1,14 @@
# Complétion bash pour « git deploy » et « git hotfix ». # Complétion bash pour « git deploy » et « git hotfix ».
# Complète le 1er argument avec les remotes, le 2e avec les branches locales, # git deploy : 1er arg = remote, args suivants = branches (multi-branches).
# le tout lu en direct depuis le dépôt courant. # 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. : # À 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() { # $1 = "multi" pour autoriser plusieurs branches, sinon une seule.
local i args=() cur="${COMP_WORDS[COMP_CWORD]}" _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) # Recense les arguments positionnels déjà saisis (hors options et valeur de -m)
for ((i = 2; i < COMP_CWORD; i++)); do for ((i = 2; i < COMP_CWORD; i++)); do
case "${COMP_WORDS[i-1]}" in -m|--message) continue ;; esac case "${COMP_WORDS[i-1]}" in -m|--message) continue ;; esac
@@ -14,13 +16,25 @@ _deploy_complete_positional() {
args+=("${COMP_WORDS[i]}") args+=("${COMP_WORDS[i]}")
done done
case "${#args[@]}" in local n=${#args[@]}
0) COMPREPLY=($(compgen -W "$(git remote 2>/dev/null)" -- "$cur")) ;; if [ "$n" -eq 0 ]; then
1) COMPREPLY=($(compgen -W "$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null)" -- "$cur")) ;; COMPREPLY=($(compgen -W "$(git remote 2>/dev/null)" -- "$cur"))
*) COMPREPLY=() ;; return
esac 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. # Hooks reconnus par la complétion git pour les sous-commandes personnalisées.
_git_deploy() { _deploy_complete_positional; } _git_deploy() { _deploy_complete multi; }
_git_hotfix() { _deploy_complete_positional; } _git_hotfix() { _deploy_complete single; }
+34 -20
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# git-deploy <remote> <branche> -m "message" # git-deploy <remote> <branche...> -m "message"
# #
# Depuis la branche de dev : commit (message obligatoire s'il y a des modifs) # 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. # 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é. # 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 # shellcheck source=_deploy_lib.sh
. "$DIR/_deploy_lib.sh" . "$DIR/_deploy_lib.sh"
usage() { echo 'Usage : git deploy <remote> <branche> -m "message"'; } usage() { echo 'Usage : git deploy <remote> <branche...> -m "message"'; }
SOURCE=$(deploy_source) SOURCE=$(deploy_source)
PUSH=$(deploy_push) PUSH=$(deploy_push)
@@ -30,13 +31,19 @@ while [ $# -gt 0 ]; do
*) POS+=("$1"); shift ;; *) POS+=("$1"); shift ;;
esac esac
done done
REMOTE="${POS[0]:-}"; BRANCH="${POS[1]:-}" REMOTE="${POS[0]:-}"
{ [ -n "$REMOTE" ] && [ -n "$BRANCH" ]; } || { usage; exit 1; } BRANCHES=("${POS[@]:1}")
{ [ -n "$REMOTE" ] && [ "${#BRANCHES[@]}" -ge 1 ]; } || { usage; exit 1; }
# ---- Validations pré-vol (aucune mutation) --------------------------------- # ---- Validations pré-vol (aucune mutation) ---------------------------------
git remote get-url "$REMOTE" >/dev/null 2>&1 || die "Remote inconnu : $REMOTE" 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" seen=""
[ "$BRANCH" != "$SOURCE" ] || die "'$SOURCE' est la branche de dev, pas une branche de déploiement." 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=$(git branch --show-current)
[ "$current" = "$SOURCE" ] || die "Place-toi sur '$SOURCE' pour déployer (git switch $SOURCE)." [ "$current" = "$SOURCE" ] || die "Place-toi sur '$SOURCE' pour déployer (git switch $SOURCE)."
assert_clean_git_state assert_clean_git_state
@@ -47,24 +54,28 @@ if [ "$dirty" = 1 ]; then
fi fi
# ---- Confirmation ---------------------------------------------------------- # ---- 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)" [ "$dirty" = 1 ] && commit_line="commit sur '$SOURCE' : \"$MSG\"" || commit_line="aucun commit (arbre propre)"
confirm " • $commit_line confirm " • $commit_line
• merge '$SOURCE' → '$BRANCH' • merge '$SOURCE' → $list
• push : $push_line" • push : $push_line"
# ---- Sauvegarde de l'état pour rollback ------------------------------------ # ---- Sauvegarde de l'état pour rollback ------------------------------------
SAVED_SOURCE=$(git rev-parse --verify "refs/heads/$SOURCE") 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 COMMITTED=0
rollback() { rollback() {
set +e set +e
echo "↩ Restauration de l'état initial…" >&2 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 [ -e "$gd/MERGE_HEAD" ] && git merge --abort
git reset --hard -q 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" git switch -q "$SOURCE" 2>/dev/null || git switch -q --force "$SOURCE"
if [ "$COMMITTED" = 1 ]; then if [ "$COMMITTED" = 1 ]; then
# défait le commit mais garde les fichiers modifiés (état d'origine) # 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" echo "✓ Commit sur '$SOURCE' : $MSG"
fi fi
git switch -q "$BRANCH" || fail "Impossible de basculer sur '$BRANCH'." for b in "${BRANCHES[@]}"; do
git merge --no-edit -q "$SOURCE" || fail "Conflit de merge sur '$BRANCH'." 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'." 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é ------------------------- # ---- Push : dernière étape, atomique, jamais forcé -------------------------
if [ "$PUSH" = true ]; then if [ "$PUSH" = true ]; then
echo "→ Vérification du push (dry-run)…" 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 rejeté au dry-run (non fast-forward ?). RIEN poussé. Local conservé." >&2; exit 1; }
echo "→ Push atomique '$BRANCH' + '$SOURCE' → '$REMOTE'…" echo "→ Push atomique ($list + '$SOURCE') → '$REMOTE'…"
git push --atomic "$REMOTE" "$BRANCH" "$SOURCE" \ git push --atomic "$REMOTE" "${BRANCHES[@]}" "$SOURCE" \
|| { echo "✗ Échec du push. RIEN poussé (atomique). Local conservé." >&2; \ || { 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'." echo "✓ Poussé sur '$REMOTE'."
else 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 fi
echo "✓ Terminé. Tu es sur '$SOURCE'." echo "✓ Terminé. Tu es sur '$SOURCE'."