Compare commits

...
4 Commits
4 changed files with 128 additions and 59 deletions
+50 -18
View File
@@ -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 <remote> <branche> -m "message" # livrer le dev vers un environnement
git hotfix <remote> <branche> -m "message" # corriger un environnement puis rapatrier dans le dev
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
```
## Modèle de branches
@@ -21,25 +21,37 @@ main**, puis rapatrie automatiquement le correctif dans le dev.
## 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
# dans le dépôt qui doit déployer
git submodule add https://figureslibres.io/gitea/bachir/gitploy.git gitploy
git submodule update --init
gitploy/setup
```
### En dépôt autonome
```bash
git clone gitea-figureslibres.io:bachir/gitploy.git
cd gitploy && ./setup # configure le dépôt parent courant
gitploy/setup # installe des alias git locaux (sans PATH)
```
`setup` installe des **alias git locaux** (`git deploy`, `git hotfix`) pointant
vers les scripts**aucune modification du `PATH`**. git n'exécutant jamais une
config versionnée tout seul (par sécurité), cette étape unique est nécessaire.
vers les scripts. git n'exécutant jamais une config versionnée tout seul (par
sécurité), cette étape est nécessaire à chaque clone — c'est le prix du submodule.
## 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) ;
2. merge `master``prod` ;
3. push atomique (dernière étape) ;
3. push de `prod` (dernière étape) ;
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)
```bash
@@ -81,11 +111,13 @@ git hotfix gitea prod -m "fix: lien du menu"
1. met tes modifs de côté (stash) ;
2. bascule sur `prod`, applique et committe le correctif ;
3. rapatrie le correctif dans `master` (merge) ;
4. push atomique puis retour sur `master`.
3. rapatrie le correctif dans `master` en local (merge) ;
4. push de `prod` puis retour sur `master`.
Résultat : le correctif est sur `prod` **et** sur `master`, sans embarquer les
features non livrées de `master`.
Résultat : le correctif est sur `prod` (poussé) **et** dans ton `master` local,
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é
+26 -12
View File
@@ -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; }
+44 -21
View File
@@ -1,8 +1,9 @@
#!/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)
# → 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é.
@@ -12,11 +13,11 @@
# git config deploy.push true|false (défaut : false — push désactivé)
set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
DIR=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)
# shellcheck source=_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)
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"
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" = "$SOURCE" ] || die "Place-toi sur '$SOURCE' pour déployer (git switch $SOURCE)."
assert_clean_git_state
@@ -47,24 +54,33 @@ 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[*]}"
# 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)"
[ -n "$merge_list" ] && merge_line="merge '$SOURCE' → $merge_list" || merge_line="aucun merge (push direct)"
confirm " • $commit_line
• merge '$SOURCE' → '$BRANCH'
$merge_line
• 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 +100,29 @@ 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
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'."
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[@]}" \
|| { 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) → '$REMOTE'…"
git push --atomic "$REMOTE" "${BRANCHES[@]}" \
|| { 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'."
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
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é)
set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
DIR=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)
# shellcheck source=_deploy_lib.sh
. "$DIR/_deploy_lib.sh"
@@ -50,7 +50,7 @@ assert_clean_git_state
{ [ "$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
if [ "$PUSH" = true ]; then push_line="OUI vers '$REMOTE' ('$BRANCH')"; else push_line="NON (désactivé)"; fi
confirm " • stash des modifs de '$SOURCE'
• application + commit sur '$BRANCH' : \"$MSG\"
• rapatriement (merge) dans '$SOURCE'
@@ -99,14 +99,14 @@ 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" \
git push --dry-run "$REMOTE" "$BRANCH" \
|| { 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 "→ Push '$BRANCH' → '$REMOTE'…"
git push "$REMOTE" "$BRANCH" \
|| { echo "✗ Échec du push. RIEN poussé. Local conservé." >&2; \
echo " Relance : git push $REMOTE $BRANCH" >&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 $REMOTE $BRANCH"
fi
echo "✓ Terminé. Tu es sur '$SOURCE'."