Compare commits

..
4 Commits
4 changed files with 128 additions and 59 deletions
+49 -17
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
``` ```
@@ -21,25 +21,37 @@ main**, puis rapatrie automatiquement le correctif dans le dev.
## Installation ## Installation
### En submodule (recommandé) ### Au niveau utilisateur (recommandé)
gitploy est un *outil* : on l'installe une fois, tous les dépôts en profitent.
```bash
# 1. cloner une fois dans un emplacement stable
git clone gitea-figureslibres.io:bachir/gitploy.git ~/.local/share/gitploy
# 2. exposer les sous-commandes (via un dossier du PATH, ex. ~/.local/bin)
ln -sf ~/.local/share/gitploy/git-deploy ~/.local/bin/git-deploy
ln -sf ~/.local/share/gitploy/git-hotfix ~/.local/bin/git-hotfix
# 3. complétion (une fois)
echo 'source "$HOME/.local/share/gitploy/deploy-completion.bash"' >> ~/.bash_completion
```
`git deploy` / `git hotfix` sont alors disponibles dans **tous** tes dépôts,
sans alias ni `setup`. Mise à jour globale : `git -C ~/.local/share/gitploy pull`.
### En submodule (pour figer une version par projet)
```bash ```bash
# dans le dépôt qui doit déployer # dans le dépôt qui doit déployer
git submodule add https://figureslibres.io/gitea/bachir/gitploy.git gitploy git submodule add https://figureslibres.io/gitea/bachir/gitploy.git gitploy
git submodule update --init git submodule update --init
gitploy/setup gitploy/setup # installe des alias git locaux (sans PATH)
```
### En dépôt autonome
```bash
git clone gitea-figureslibres.io:bachir/gitploy.git
cd gitploy && ./setup # configure le dépôt parent courant
``` ```
`setup` installe des **alias git locaux** (`git deploy`, `git hotfix`) pointant `setup` installe des **alias git locaux** (`git deploy`, `git hotfix`) pointant
vers les scripts**aucune modification du `PATH`**. git n'exécutant jamais une vers les scripts. git n'exécutant jamais une config versionnée tout seul (par
config versionnée tout seul (par sécurité), cette étape unique est nécessaire. sécurité), cette étape est nécessaire à chaque clone — c'est le prix du submodule.
## Configuration ## Configuration
@@ -69,9 +81,27 @@ git deploy gitea prod -m "ajout du bloc contact"
1. commit sur `master` (message obligatoire s'il y a des modifs) ; 1. commit sur `master` (message obligatoire s'il y a des modifs) ;
2. merge `master``prod` ; 2. merge `master``prod` ;
3. push atomique (dernière étape) ; 3. push de `prod` (dernière étape) ;
4. retour sur `master`. 4. retour sur `master`.
> `git deploy` ne pousse **que les branches nommées**, jamais `master` implicitement.
> Ça évite qu'un bare repo serveur (dont le hook n'attend qu'une branche précise)
> rejette le push.
>
> Pour sauvegarder `master` sur un remote central, inclus-le explicitement dans la
> liste — il sera poussé tel quel (sans merge) :
>
> ```bash
> git deploy gitea master prod stage -m "message"
> ```
Plusieurs environnements en une commande (commit unique, merges enchaînés,
**push atomique de toutes les branches nommées à 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
@@ -81,11 +111,13 @@ git hotfix gitea prod -m "fix: lien du menu"
1. met tes modifs de côté (stash) ; 1. met tes modifs de côté (stash) ;
2. bascule sur `prod`, applique et committe le correctif ; 2. bascule sur `prod`, applique et committe le correctif ;
3. rapatrie le correctif dans `master` (merge) ; 3. rapatrie le correctif dans `master` en local (merge) ;
4. push atomique puis retour sur `master`. 4. push de `prod` puis retour sur `master`.
Résultat : le correctif est sur `prod` **et** sur `master`, sans embarquer les Résultat : le correctif est sur `prod` (poussé) **et** dans ton `master` local,
features non livrées de `master`. sans embarquer les features non livrées de `master`. Comme `git deploy`, `git
hotfix` ne pousse **que** la branche d'env, pas `master` : ton `master` distant
se mettra à jour quand tu le pousseras explicitement.
## Sûreté ## Sûreté
+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; }
+44 -21
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é.
@@ -12,11 +13,11 @@
# git config deploy.push true|false (défaut : false — push désactivé) # git config deploy.push true|false (défaut : false — push désactivé)
set -euo pipefail set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) DIR=$(cd "$(dirname "$(readlink -f "${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"
case " $seen " in *" $b "*) die "Branche indiquée en double : $b" ;; esac
seen="$seen $b"
done
# '$SOURCE' est autorisée dans la liste : elle sera poussée telle quelle (pas de merge).
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,33 @@ 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[*]}"
# branches réellement fusionnées = toutes sauf SOURCE (qui est poussée telle quelle)
merge_list=""
for b in "${BRANCHES[@]}"; do [ "$b" != "$SOURCE" ] && merge_list="$merge_list $b"; done
merge_list="${merge_list# }"
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)" [ "$dirty" = 1 ] && commit_line="commit sur '$SOURCE' : \"$MSG\"" || commit_line="aucun commit (arbre propre)"
[ -n "$merge_list" ] && merge_line="merge '$SOURCE' → $merge_list" || merge_line="aucun merge (push direct)"
confirm " • $commit_line confirm " • $commit_line
• merge '$SOURCE' → '$BRANCH' $merge_line
• 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 +100,29 @@ 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'." if [ "$b" = "$SOURCE" ]; then
echo "✓ '$b' (dev) incluse au push telle quelle."
continue
fi
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[@]}" \
|| { 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) → '$REMOTE'…"
git push --atomic "$REMOTE" "$BRANCH" "$SOURCE" \ git push --atomic "$REMOTE" "${BRANCHES[@]}" \
|| { 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" >&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"
fi fi
echo "✓ Terminé. Tu es sur '$SOURCE'." echo "✓ Terminé. Tu es sur '$SOURCE'."
+8 -8
View File
@@ -18,7 +18,7 @@
# git config deploy.push true|false (défaut : false — push désactivé) # git config deploy.push true|false (défaut : false — push désactivé)
set -euo pipefail set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) DIR=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)
# shellcheck source=_deploy_lib.sh # shellcheck source=_deploy_lib.sh
. "$DIR/_deploy_lib.sh" . "$DIR/_deploy_lib.sh"
@@ -50,7 +50,7 @@ assert_clean_git_state
{ [ "$MSG_SET" = 1 ] && [ -n "$MSG" ]; } || die 'Message requis : -m "…".' { [ "$MSG_SET" = 1 ] && [ -n "$MSG" ]; } || die 'Message requis : -m "…".'
# ---- Confirmation ---------------------------------------------------------- # ---- Confirmation ----------------------------------------------------------
if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ('$BRANCH' + '$SOURCE', atomique)"; else push_line="NON (désactivé)"; fi if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ('$BRANCH')"; else push_line="NON (désactivé)"; fi
confirm " • stash des modifs de '$SOURCE' confirm " • stash des modifs de '$SOURCE'
• application + commit sur '$BRANCH' : \"$MSG\" • application + commit sur '$BRANCH' : \"$MSG\"
• rapatriement (merge) dans '$SOURCE' • rapatriement (merge) dans '$SOURCE'
@@ -99,14 +99,14 @@ echo "✓ Local OK : hotfix sur '$BRANCH' + rapatrié dans '$SOURCE'."
# ---- 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 --dry-run "$REMOTE" "$BRANCH" \
|| { 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 '$BRANCH' → '$REMOTE'…"
git push --atomic "$REMOTE" "$BRANCH" "$SOURCE" \ git push "$REMOTE" "$BRANCH" \
|| { echo "✗ Échec du push. RIEN poussé (atomique). Local conservé." >&2; \ || { echo "✗ Échec du push. RIEN poussé. Local conservé." >&2; \
echo " Relance : git push --atomic $REMOTE $BRANCH $SOURCE" >&2; exit 1; } echo " Relance : git push $REMOTE $BRANCH" >&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 $REMOTE $BRANCH"
fi fi
echo "✓ Terminé. Tu es sur '$SOURCE'." echo "✓ Terminé. Tu es sur '$SOURCE'."