Git es el sistema de control de versiones más usado del mundo. Pero usar Git a mano para todo — subir versiones, generar changelogs, limpiar ramas, desplegar, verificar hooks — es tedioso, propenso a errores y nada escalable.
Bash es el lenguaje natural para automatizar Git porque:
- Git es un programa de línea de comandos — todo lo que haces en la GUI se puede hacer desde Bash
- Los hooks de Git son scripts — son Bash (o cualquier intérprete) que se ejecutan en eventos específicos
- No necesitas herramientas externas —
git,grep,awk,sed,sort,datees todo lo que necesitas - Integración con CI/CD — los pipelines de GitHub Actions, GitLab CI, Jenkins, todos ejecutan Bash
En este capítulo verás como,
- Escribir hooks de Git profesionales para pre-commit, post-commit, pre-push y post-merge
- Parsear el log de Git con formato personalizado y generar changelogs automáticos
- Detectar el estado del repositorio (sucio, rama actual, ahead/behind) desde scripts
- Automatizar tags, version bump, despliegues, limpieza de ramas y backups
- Construir un script de automatización Git completo y reutilizable
Filosofía del capítulo: Git no es solo
add,commit,push. Es una base de datos de tu proyecto, y Bash es el lenguaje para consultarla, transformarla y actuar sobre ella automáticamente.
Git Hooks — El punto de entrada
Anatomía de un hook
Los hooks de Git son scripts que Git ejecuta automáticamente cuando ocurren ciertos eventos. Viven en .git/hooks/ de cada repositorio (no se versionan) o se configuran globalmente con core.hooksPath.
# hook es simplemente un script ejecutable
# le pasa información por stdin y argumentos
#!/bin/bash
#
echo "Ejecutando pre-commit hook..."
# el script sale con código distinto de 0, Git aborta la acción
Tipos de hooks por fase:
| Hook | Evento | ¿Puede abortar? | Argumentos |
|---|---|---|---|
pre-commit | Antes de crear el commit | Sí | Ninguno |
prepare-commit-msg | Antes de abrir el editor de mensaje | Sí | Archivo, tipo, SHA (merge) |
commit-msg | Después de escribir el mensaje | Sí | Archivo del mensaje |
post-commit | Después de crear el commit | No | Ninguno |
pre-push | Antes de enviar al remoto | Sí | Remoto, URL |
pre-receive | En el servidor, antes de recibir push | Sí | Ninguno (lee stdin) |
update | Por cada ref en el servidor | Sí | Ref, SHA anterior, SHA nuevo |
post-receive | En el servidor, tras recibir push | No | Ninguno (lee stdin) |
post-merge | Después de un merge exitoso | No | 1 si squash |
pre-auto-gc | Antes de git gc --auto | Sí | Ninguno |
pre-commit — Validar antes de confirmar
El hook pre-commit se ejecuta antes de que se cree el commit. Si falla (exit ≠ 0), el commit se cancela. Es el lugar perfecto para linters, formateadores, verificaciones de seguridad, tests rápidos.
#!/bin/bash
# — Validaciones antes de cada commit
set -euo pipefail
echo "🔍 pre-commit: ejecutando validaciones..."
# Verificar que no hay conflictos de merge
if grep -rI '^<<<<<<< ' --include='*.py' --include='*.js' --include='*.sh' . 2>/dev/null; then
echo "❌ ERROR: Hay marcadores de conflicto sin resolver"
exit 1
fi
# Verificar que no hay credenciales (patrón básico)
if git diff --cached -U0 | grep -Pi '(password|secret|api.?key|token)\s*[:=]\s*["'"'"']?[^"'"'"'\s]{8,}' > /dev/null; then
echo "❌ ERROR: Posible credencial en el diff"
exit 1
fi
# Verificar que no hay archivos binarios grandes (> 1 MB)
large_files=$(git diff --cached --name-only -z | xargs -0 -I{} sh -c '
[ -f "{}" ] && [ "$(stat -f%z "{}" 2>/dev/null || stat --format=%s "{}" 2>/dev/null)" -gt 1048576 ] && echo "{}"
')
if [ -n "$large_files" ]; then
echo "⚠️ Archivos grandes (> 1MB) en el commit:"
echo "$large_files"
echo "¿Continuar de todas formas? (s/N): "
read -r respuesta
[ "$respuesta" != "s" ] && exit 1
fi
# Shellcheck en scripts Bash (si está instalado)
if command -v shellcheck &>/dev/null; then
while IFS= read -r -d '' script; do
echo " shellcheck: $script"
if ! shellcheck -x "$script" 2>/dev/null; then
echo "❌ shellcheck falló en $script"
exit 1
fi
done < <(git diff --cached --name-only -z -- '*.sh')
fi
echo "✅ pre-commit: validaciones pasadas"
post-commit — Reaccionar tras confirmar
post-commit se ejecuta después de crear el commit. No puede abortar, pero es ideal para notificaciones, logs, o disparar procesos.
#!/bin/bash
# — Notificar commit
set -euo pipefail
commit_hash=$(git rev-parse HEAD)
commit_msg=$(git log -1 --format='%s')
branch=$(git branch --show-current)
# a Telegram (ejemplo)
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${TELEGRAM_CHAT_ID:-}" ]; then
mensaje="🔨 Commit en $branch: $commit_msg ($commit_hash)"
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=$TELEGRAM_CHAT_ID&text=$mensaje" &>/dev/null
fi
# en log local
echo "[$(date '+%Y-%m-%d %H:%M:%S')] COMMIT $commit_hash en $branch: $commit_msg" \
>> "$HOME/.git_commit_log"
pre-push — Bloquear push inseguros
pre-push se ejecuta antes de enviar objetos al remoto. Recibe el nombre del remoto y su URL como argumentos, y por stdin recibe líneas con el formato <local-ref> <local-sha> <remote-ref> <remote-sha>.
#!/bin/bash
# — Validar antes de hacer push
set -euo pipefail
remote="$1"
url="$2"
echo "🔒 pre-push: validando push a $remote..."
# No permitir push directo a main/master (a menos que sea --force consciente)
current_branch=$(git branch --show-current)
protected_branches="main master develop"
for pb in $protected_branches; do
if [ "$current_branch" = "$pb" ]; then
# Si es push forzado, permitir (quien fuerza sabe lo que hace)
# Pero pedir confirmación
echo "⚠️ Estás haciendo push a '$pb' (rama protegida)"
echo "¿Continuar? (s/N): "
read -r respuesta
[ "$respuesta" != "s" ] && exit 1
fi
done
# Verificar que los tests pasan (si existe script de test)
if [ -f "test.sh" ] || [ -f "tests/test.sh" ]; then
echo " Ejecutando tests..."
if [ -f "test.sh" ]; then
bash test.sh || { echo "❌ Tests fallaron"; exit 1; }
fi
fi
echo "✅ pre-push: validaciones pasadas, push permitido"
post-merge — Reaccionar tras fusionar
Útil para actualizar dependencias, migraciones de base de datos, o reconstruir algo después de un merge.
#!/bin/bash
# — Reaccionar tras merge
set -euo pipefail
echo "🔀 post-merge: actualizando después del merge..."
# recibimos '1' como argumento, fue un squash merge
squash="${1:-0}"
[ "$squash" = "1" ] && echo " (squash merge detectado)"
# dependencias si package.json o requirements.txt cambió
if git diff HEAD@{1} --name-only | grep -qE '^(package.json|requirements\.txt)$'; then
echo " Dependencias cambiadas — reinstalando..."
if [ -f "package.json" ]; then
npm install &>/dev/null && echo " npm install OK"
fi
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt &>/dev/null && echo " pip install OK"
fi
fi
# assets si hubo cambios en src/
if git diff HEAD@{1} --name-only | grep -q '^src/'; then
echo " Código fuente cambiado — reconstruyendo..."
if [ -f "Makefile" ]; then
make build &>/dev/null && echo " build OK"
fi
fi
pre-receive — Hooks del lado del servidor
Los hooks del lado del servidor (pre-receive, update, post-receive) viven en el repositorio remoto (el del servidor Git) y se ejecutan cuando alguien hace push. Son la última línea de defensa.
#!/bin/bash
# — En el servidor, validar antes de aceptar push
set -euo pipefail
# recibe por stdin: <old-sha> <new-sha> <ref>
# ejemplo: 0000... abcd... refs/heads/main
while read -r old_sha new_sha ref; do
branch="${ref#refs/heads/}"
# Proteger ramas específicas
case "$branch" in
main|master|production)
echo "❌ Push a '$branch' no permitido mediante pre-receive"
exit 1
;;
esac
# Verificar que el commit está firmado (GPG)
if ! git verify-commit "$new_sha" &>/dev/null; then
echo "❌ El commit $new_sha no está firmado GPG"
exit 1
fi
# Verificar tamaño máximo del push
size=$(git rev-list --count "$old_sha..$new_sha" 2>/dev/null || echo 0)
if [ "$size" -gt 100 ]; then
echo "❌ Más de 100 commits en un solo push no está permitido"
exit 1
fi
done
⚠️ Nota: Para usar hooks del lado del servidor en un repositorio bare, colócalos en
/ruta/al/repo.git/hooks/. En GitHub/GitLab esto se hace con reglas de protección de rama y acciones CI, no con hooks locales.
Git Log — Minería de datos del repositorio
Git almacena toda la historia de tu proyecto. Con Bash puedes extraer, filtrar y transformar esa información de formas que ninguna GUI permite.
Formatos de salida de git log
# formato más compacto
git log --oneline
# feat: añadir autenticación OAuth
# fix: corregir timeout en conexión
# personalizado con --format
git log --format="%h | %an | %ar | %s"
# | Lorenzo | 2 days ago | feat: añadir autenticación OAuth
# gráfico de ramas
git log --oneline --graph --all
# a1b2c3d (HEAD -> main) merge
#
# * e5f6g7h (feature/login) fix login redirect
# | 8i9j0k1 chore: update deps
#
# rango de fechas
git log --since="2025-01-01" --until="2025-06-30" --oneline
# autor
git log --author="Lorenzo" --oneline
# modificados en cada commit
git log --name-only --oneline
# (líneas +- por commit)
git log --stat --oneline
Formatos de --format más útiles:
| Placeholder | Significado | Ejemplo |
|---|---|---|
%H | SHA completo | a1b2c3d4e5f6... |
%h | SHA abreviado | a1b2c3d |
%an | Author name | Lorenzo |
%ae | Author email | lorenzo@example.com |
%ar | Fecha relativa | 2 days ago |
%ai | Fecha ISO-8601 | 2025-06-11 10:30:00 +0000 |
%s | Subject (mensaje) | feat: añadir login |
%b | Body | Cuerpo del mensaje |
%D | Ref names | HEAD -> main, tag: v1.0 |
%G? | Firma GPG | G (good) / B (bad) |
Parsear author, fecha y mensaje
#!/bin/bash
# datos estructurados del log
# commits por autor con conteo
git log --format="%an" --since="2025-01-01" | sort | uniq -c | sort -rn
# 42 Lorenzo
# 15 María
# 8 Carlos
# el último commit de cada autor
git log --format="%an | %h | %s" | awk -F'|' '!seen[$1]++'
# | a1b2c3d | feat: añadir login
# | e5f6g7h | fix: corregir timeout
# por día de la semana (¿cuándo trabajas más?)
git log --date=format:"%u" --format="%ad" | sort | uniq -c | sort -rn
# 18 1 (lunes)
# 15 3 (miércoles)
# 12 2 (martes)
# 8 5 (viernes)
# 6 4 (jueves)
# fecha ISO para cálculos
while IFS='|' read -r sha date author message; do
year="${date:0:4}"
month="${date:5:2}"
echo "$year-$month: $sha por $author → $message"
done < <(git log --format="%h|%ai|%an|%s")
Generar changelog desde el log
Una de las automatizaciones más útiles: generar un CHANGELOG.md a partir de los mensajes de commit, agrupados por versión.
#!/bin/bash
# — Genera CHANGELOG.md desde git log
# ./generar_changelog.sh [desde_tag] [hasta_tag]
desde="${1:-$(git tag --sort=-creatordate | head -1)}"
hasta="${2:-HEAD}"
if [ -z "$desde" ]; then
# Sin tags: todo el historial
rango="HEAD"
nuevo=true
else
rango="${desde}..${hasta}"
nuevo=false
fi
echo "# Changelog"
echo ""
echo "## [$hasta] - $(date +%Y-%m-%d)"
echo ""
# commits agrupados por tipo (Conventional Commits)
git log "$rango" --format="%s" --reverse | while IFS= read -r line; do
case "$line" in
feat:*|feature:*) echo "### 🚀 Nuevas funcionalidades" >&3; echo "- $line" ;;
fix:*) echo "### 🐛 Correcciones" >&3; echo "- $line" ;;
chore:*|refactor:*) echo "### 🔧 Mantenimiento" >&3; echo "- $line" ;;
docs:*) echo "### 📚 Documentación" >&3; echo "- $line" ;;
test:*) echo "### 🧪 Tests" >&3; echo "- $line" ;;
perf:*) echo "### ⚡ Rendimiento" >&3; echo "- $line" ;;
*) echo "### Otros cambios" >&3; echo "- $line" ;;
esac
done 3>/tmp/changelog_sections.tmp
cat /tmp/changelog_sections.tmp 2>/dev/null
rm -f /tmp/changelog_sections.tmp
Versión más sofisticada: changelog por tags
#!/bin/bash
# — Changelog completo entre versiones etiquetadas
set -euo pipefail
tags=$(git tag --sort=creatordate)
prev=""
echo "# Changelog"
echo ""
for tag in $tags; do
if [ -z "$prev" ]; then
rango="$tag"
else
rango="$prev..$tag"
fi
fecha=$(git log -1 --format="%ai" "$tag" 2>/dev/null | cut -d' ' -f1)
echo "## [$tag] - $fecha"
echo ""
# Agrupar por tipo
git log "$rango" --format="%s" --reverse 2>/dev/null | while IFS= read -r line; do
tipo="${line%%:*}"
mensaje="${line#*: }"
echo "- **$tipo**: $mensaje"
done
echo ""
prev="$tag"
done
# cambios no etiquetados
if git log "$prev..HEAD" --oneline 2>/dev/null | grep -q .; then
echo "## [Sin publicar] - $(date +%Y-%m-%d)"
echo ""
git log "$prev..HEAD" --format="- %s" --reverse 2>/dev/null
fi
Estado del repositorio
Saber en qué estado está tu repositorio es esencial para cualquier automatización.
¿Working tree sucio? — git status –porcelain
git status --porcelain devuelve una salida estable (sin colores, sin adornos) ideal para scripts.
#!/bin/bash
# si hay cambios sin commit
# 1: git status --porcelain
if [ -n "$(git status --porcelain)" ]; then
echo "⚠️ Working tree sucio: hay cambios sin commit"
git status --porcelain | head -10
else
echo "✅ Working tree limpio"
fi
# 2: diff-index (más rápido)
if ! git diff-index --quiet HEAD --; then
echo "⚠️ Hay cambios sin commitear"
fi
# staged pero no commiteados
if [ -n "$(git diff --cached --name-only)" ]; then
echo "📦 Archivos staged listos para commit:"
git diff --cached --name-only
fi
# sin seguimiento (untracked)
untracked=$(git ls-files --others --exclude-standard)
if [ -n "$untracked" ]; then
echo "📄 Archivos sin seguimiento:"
echo "$untracked" | head -10
fi
¿En qué rama estoy?
#!/bin/bash
# rama actual de forma robusta
# 1: git branch --show-current (Git 2.22+)
branch=$(git branch --show-current)
# 2: git rev-parse (fallback universal)
branch=$(git rev-parse --abbrev-ref HEAD)
# estado detached HEAD
if [ "$branch" = "HEAD" ]; then
echo "⚠️ Estado detached HEAD en $(git rev-parse HEAD)"
fi
# si es rama de feature (no main/master/develop)
case "$branch" in
main|master|develop) echo "🏠 Rama principal: $branch" ;;
feature/*) echo "🚧 Feature branch: $branch" ;;
hotfix/*) echo "🔧 Hotfix branch: $branch" ;;
release/*) echo "📦 Release branch: $branch" ;;
*) echo "🌿 Rama: $branch" ;;
esac
¿Ahead/Behind del remoto?
#!/bin/bash
# divergencia con el remoto
remote_branch="${1:-origin/$(git branch --show-current)}"
# ahead (no enviados)
ahead=$(git rev-list --count "$remote_branch..HEAD" 2>/dev/null || echo 0)
# behind (por recibir)
behind=$(git rev-list --count "HEAD..$remote_branch" 2>/dev/null || echo 0)
echo "📊 Estado vs $remote_branch:"
echo " Ahead: $ahead commits (por enviar)"
echo " Behind: $behind commits (por recibir)"
if [ "$ahead" -gt 0 ] && [ "$behind" -gt 0 ]; then
echo "⚠️ Divergencia detectada — necesitas rebase o merge"
elif [ "$ahead" -gt 0 ]; then
echo "💡 Haz git push para enviar tus cambios"
elif [ "$behind" -gt 0 ]; then
echo "💡 Haz git pull para actualizarte"
else
echo "✅ Alineado con el remoto"
fi
Integridad y limpieza
#!/bin/bash
# del repositorio
# integridad del repositorio
echo "🔍 Verificando integridad..."
if git fsck --no-dangling 2>&1 | grep -qE '(error|missing)'; then
echo "⚠️ Problemas de integridad detectados"
git fsck --no-dangling
else
echo "✅ Repositorio íntegro"
fi
# objetos no referenciados
echo "🧹 Limpiando objetos inalcanzables..."
git gc --auto --prune=now 2>/dev/null && echo "✅ gc completado"
# del repositorio
size=$(du -sh .git | cut -f1)
echo "📦 Tamaño del .git: $size"
Tag Management
Crear tags anotados y ligeros
# ligero (solo un puntero, sin metadatos)
git tag v1.0.0
# anotado (con mensaje, autor, fecha — recomendado para releases)
git tag -a v1.0.0 -m "Release v1.0.0: Primera versión estable"
# firmado con GPG
git tag -s v1.0.0 -m "Release v1.0.0 firmada"
# tags
git tag -l
git tag --sort=-creatordate # Más recientes primero
# detalles de un tag
git show v1.0.0
Push de tags
# todos los tags
git push --tags
# un tag específico
git push origin v1.0.0
# tag local y remoto
git tag -d v1.0.0
git push --delete origin v1.0.0
Version bump automático
#!/bin/bash
# — Incrementa versión semántica automáticamente
# version_bump.sh [major|minor|patch]
# en Conventional Commits (Angular style)
set -euo pipefail
bump_type="${1:-patch}"
last_tag=$(git tag --sort=-creatordate | head -1)
if [ -z "$last_tag" ]; then
echo "⚠️ No hay tags previos — Starting desde v0.1.0"
new_version="v0.1.0"
else
# Extraer major.minor.patch del tag (eliminando 'v' si existe)
version="${last_tag#v}"
IFS='.' read -r major minor patch <<< "$version"
case "$bump_type" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "❌ Tipo inválido: $bump_type (major|minor|patch)"
exit 1
;;
esac
new_version="v${major}.${minor}.${patch}"
fi
# el tag
git tag -a "$new_version" -m "Release $new_version"
echo "✅ Tag creado: $new_version"
# si hacer push
echo "¿Hacer push del tag? (s/N): "
read -r respuesta
if [ "$respuesta" = "s" ]; then
git push origin "$new_version"
echo "✅ Push de $new_version completado"
fi
# changelog del último release
echo ""
echo "📋 Cambios desde $last_tag:"
if [ -n "$last_tag" ]; then
git log "$last_tag..HEAD" --oneline --reverse
else
git log --oneline --reverse | head -20
fi
Versión avanzada con detección automática del tipo de bump:
# — Detecta automáticamente major/minor/patch
# los Conventional Commits desde el último tag
last_tag=$(git tag --sort=-creatordate | head -1)
[ -z "$last_tag" ] && last_tag="HEAD~0"
# breaking changes (commit con ! o BREAKING CHANGE)
if git log "$last_tag..HEAD" --format="%s" | grep -qE '!\:|BREAKING CHANGE'; then
bump_type="major"
# features
elif git log "$last_tag..HEAD" --format="%s" | grep -qE '^feat'; then
bump_type="minor"
else
bump_type="patch"
fi
echo "🔍 Detección automática: $bump_type"
exec bash "$0" "$bump_type"
Estrategias de automatización
Auto-deploy: pull + build + restart
Un script que se ejecuta en el servidor de producción, tira de los últimos cambios, construye y reinicia.
#!/bin/bash
# — Despliegue automático
# para ejecutar via webhook, cron, o post-merge
set -euo pipefail
DEPLOY_DIR="/var/www/mi-app"
BRANCH="main"
SERVICE_NAME="mi-app"
LOG_FILE="/var/log/deploy.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
log "🚀 Iniciando despliegue..."
cd "$DEPLOY_DIR"
# Verificar estado
log "📡 Verificando rama..."
current=$(git branch --show-current)
if [ "$current" != "$BRANCH" ]; then
log "❌ Rama actual es '$current', se esperaba '$BRANCH'"
exit 1
fi
# Guardar hash actual para comparar
old_hash=$(git rev-parse HEAD)
# Pull
log "📥 Haciendo pull..."
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
new_hash=$(git rev-parse HEAD)
if [ "$old_hash" = "$new_hash" ]; then
log "✅ Sin cambios nuevos — deploy no necesario"
exit 0
fi
log "📋 Cambios: $(git log --oneline "$old_hash..$new_hash" | wc -l) commits"
# Instalar dependencias
if [ -f "package.json" ]; then
log "📦 npm install..."
npm install --production >> "$LOG_FILE" 2>&1
fi
if [ -f "requirements.txt" ]; then
log "🐍 pip install..."
pip install -r requirements.txt >> "$LOG_FILE" 2>&1
fi
# Build
if [ -f "Makefile" ] && grep -q '^build:' Makefile; then
log "🔨 make build..."
make build >> "$LOG_FILE" 2>&1
fi
# Migraciones
if [ -f "manage.py" ]; then
log "🗄️ Migraciones Django..."
python manage.py migrate >> "$LOG_FILE" 2>&1
fi
# Reiniciar servicio
log "🔄 Reiniciando servicio $SERVICE_NAME..."
sudo systemctl restart "$SERVICE_NAME" >> "$LOG_FILE" 2>&1
# Healthcheck
sleep 3
if curl -sf http://localhost/health > /dev/null 2>&1; then
log "✅ Healthcheck OK — despliegue exitoso"
else
log "⚠️ Healthcheck falló — revisar manualmente"
# Rollback opcional
# git reset --hard "$old_hash"
# sudo systemctl restart "$SERVICE_NAME"
fi
Branch cleanup — Eliminar ramas fusionadas
El clásico script de limpieza que mantiene tu repositorio ordenado.
#!/bin/bash
# — Limpiar ramas locales y remotas
set -euo pipefail
echo "🧹 Limpieza de ramas"
echo "═══════════════════"
# Ramas locales ya fusionadas en main
echo ""
echo "📌 Ramas locales fusionadas en main (candidatas a eliminar):"
git branch --merged main | grep -vE '^\*|main|master|develop' | while IFS= read -r branch; do
echo " $branch"
done
if git branch --merged main | grep -vE '^\*|main|master|develop' | grep -q .; then
echo ""
echo "¿Eliminar estas ramas locales? (s/N): "
read -r respuesta
if [ "$respuesta" = "s" ]; then
git branch --merged main | grep -vE '^\*|main|master|develop' | xargs -r git branch -d
echo "✅ Ramas locales eliminadas"
fi
fi
# Ramas remotas huérfanas (sin contraparte local activa)
echo ""
echo "📌 Ramas remotas sin actividad en 30 días:"
git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin/ |
while IFS=' ' read -r branch date; do
now=$(date +%s)
diff=$(( (now - date) / 86400 ))
if [ "$diff" -gt 30 ] && ! echo "$branch" | grep -qE 'main|master|develop|HEAD'; then
echo " $branch (último commit hace ${diff}d)"
fi
done
# Ramas remotas sin tracking local
echo ""
echo "📌 Ramas remotas sin tracking local:"
for branch in $(git branch -r | grep -vE 'origin/(main|master|develop|HEAD)'); do
local_name="${branch#origin/}"
if ! git branch | grep -qE "^\*?\s*$local_name$"; then
echo " $branch"
fi
done
Stale branch detection
#!/bin/bash
# — Detectar ramas inactivas
set -euo pipefail
THRESHOLD_DAYS="${1:-90}"
echo "🔍 Ramas sin actividad en los últimos $THRESHOLD_DAYS días"
echo "══════════════════════════════════════════════════════"
git for-each-ref --format='%(refname:short)|%(committerdate:relative)|%(committerdate:unix)' refs/heads/ |
while IFS='|' read -r branch relative unix; do
now=$(date +%s)
age=$(( (now - unix) / 86400 ))
# Saltar ramas principales
case "$branch" in
main|master|develop) continue ;;
esac
if [ "$age" -gt "$THRESHOLD_DAYS" ]; then
author=$(git log -1 --format="%an" "$branch" 2>/dev/null || echo "desconocido")
echo " $branch — $relative — último autor: $author"
fi
done
Git bisect automatizado
git bisect hace búsqueda binaria para encontrar el commit que introdujo un bug. Con Bash puedes automatizarlo completamente.
#!/bin/bash
# — Búsqueda binaria automatizada
# el commit que rompió un test
set -euo pipefail
SCRIPT_TO_TEST="/ruta/al/test.sh"
# commit actual como malo
git bisect start
git bisect bad HEAD
# un commit bueno (último release)
last_tag=$(git tag --sort=-creatordate | head -1)
if [ -n "$last_tag" ]; then
git bisect good "$last_tag"
else
# Si no hay tags, ir hacia atrás N commits
git bisect good HEAD~50 2>/dev/null || {
echo "❌ No se pudo encontrar un commit bueno"
git bisect reset
exit 1
}
fi
# bisect automático
git bisect run bash "$SCRIPT_TO_TEST"
# resultado
echo ""
echo "🔍 Resultado del bisect:"
git log -1 --oneline "$(git bisect view --oneline 2>/dev/null | head -1 | awk '{print $1}')"
git bisect reset
Git submodule management
#!/bin/bash
# — Gestión de submódulos
set -euo pipefail
cmd="${1:-status}"
case "$cmd" in
status)
echo "📦 Estado de submódulos:"
git submodule status
;;
init-update)
echo "📦 Inicializando y actualizando submódulos..."
git submodule init
git submodule update --recursive
echo "✅ Submódulos actualizados"
;;
latest)
echo "📦 Actualizando cada submódulo a su última versión..."
git submodule foreach --recursive 'git checkout main && git pull origin main'
echo "✅ Submódulos en su última versión"
;;
push-recursive)
echo "📦 Haciendo push recursivo..."
git submodule foreach --recursive 'git push origin HEAD'
echo "✅ Push recursivo completado"
;;
diff)
echo "📦 Diferencia de submódulos:"
git diff --submodule
;;
*)
echo "Uso: $0 {status|init-update|latest|push-recursive|diff}"
;;
esac
Backup con git bundle
git bundle crea un solo archivo con todo el repositorio — ideal para backups, transferencias offline o copias de seguridad.
#!/bin/bash
# — Backup del repositorio con git bundle
set -euo pipefail
REPO_DIR="${1:-.}"
BACKUP_DIR="${2:-/tmp/git-backups}"
cd "$REPO_DIR"
# directorio de backups
mkdir -p "$BACKUP_DIR"
# del bundle con fecha
backup_file="${BACKUP_DIR}/backup_$(basename "$(pwd)")_$(date +%Y%m%d_%H%M%S).bundle"
echo "📦 Creando bundle del repositorio $(basename "$(pwd)")..."
# completo (todas las ramas y tags)
git bundle create "$backup_file" --all
# integridad del bundle
if git bundle verify "$backup_file" &>/dev/null; then
size=$(du -h "$backup_file" | cut -f1)
echo "✅ Bundle creado: $backup_file ($size)"
else
echo "❌ Bundle corrupto"
rm -f "$backup_file"
exit 1
fi
#
gzip "$backup_file"
echo "✅ Comprimido: ${backup_file}.gz"
# solo los últimos 7 backups
ls -t "$BACKUP_DIR"/*.bundle.gz 2>/dev/null | tail -n +8 | xargs -r rm
echo "🧹 Backups antiguos limpiados (últimos 7)"
Restaurar desde un bundle:
# desde un bundle
git clone backup_20250611_143000.bundle nuevo_repo
# añadirlo como remoto a un repo existente
git remote add backup /ruta/al/backup.bundle
git fetch backup
Git worktree — Múltiples ramas simultáneas
git worktree permite tener múltiples ramas checkouteadas al mismo tiempo en directorios diferentes. Bash lo hace aún más práctico.
#!/bin/bash
# — Gestiona worktrees
set -euo pipefail
WORKTREE_DIR="${HOME}/git-worktrees"
cmd="${1:-list}"
case "$cmd" in
list)
echo "📂 Worktrees activos:"
git worktree list
;;
add)
branch="${2:?Uso: $0 add <rama> [directorio]}"
dir="${3:-${WORKTREE_DIR}/${branch}}"
mkdir -p "$(dirname "$dir")"
git worktree add "$dir" "$branch"
echo "✅ Worktree creado: $dir → $branch"
;;
remove)
dir="${2:?Uso: $0 remove <directorio>}"
git worktree remove "$dir"
echo "✅ Worktree eliminado: $dir"
;;
prune)
echo "🧹 Limpiando worktrees huérfanos..."
git worktree prune
echo "✅ Worktrees huérfanos eliminados"
;;
create-features)
echo "🔨 Creando worktrees para todas las ramas feature/*..."
git branch | grep 'feature/' | while IFS= read -r branch; do
branch="${branch# }" # eliminar espacios
dir="${WORKTREE_DIR}/${branch}"
if [ ! -d "$dir" ]; then
echo " Creando worktree: $dir → $branch"
git worktree add "$dir" "$branch" 2>/dev/null || \
echo " ⚠️ Error creando worktree para $branch"
fi
done
;;
*)
echo "Uso: $0 {list|add|remove|prune|create-features}"
;;
esac
CI/CD desde Bash
Pipeline mínimo: lint + test + build + deploy
Un pipeline CI/CD no es más que una serie de comandos Bash que se ejecutan en orden. Aquí tienes un pipeline portable que funciona en cualquier sistema CI (GitHub Actions, GitLab CI, Jenkins):
#!/bin/bash
# — Pipeline CI/CD portable
set -euo pipefail
# Configuración ───
PROJECT_NAME="mi-app"
BRANCH="${CI_COMMIT_BRANCH:-${GITHUB_REF_NAME:-$(git branch --show-current)}}"
COMMIT_SHA="${CI_COMMIT_SHA:-${GITHUB_SHA:-$(git rev-parse HEAD)}}"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "════════════════════════════════════════════"
echo " Pipeline CI/CD — $PROJECT_NAME"
echo " Rama: $BRANCH"
echo " Commit: $COMMIT_SHA"
echo " Fecha: $TIMESTAMP"
echo "════════════════════════════════════════════"
# Step 1: Lint ───
echo ""
echo "🔍 [1/4] Lint — Verificando estilo y calidad..."
if command -v shellcheck &>/dev/null; then
find . -name '*.sh' -exec shellcheck -x {} + || {
echo "❌ Shellcheck encontró errores"
exit 1
}
echo "✅ Shellcheck: OK"
else
echo "⚠️ shellcheck no instalado — saltando"
fi
# Step 2: Test ───
echo ""
echo "🧪 [2/4] Test — Ejecutando pruebas..."
if [ -f "test.sh" ]; then
bash test.sh || {
echo "❌ Tests fallaron"
exit 1
}
echo "✅ Tests: OK"
else
# Tests unitarios con BATS si existe
if [ -d "tests" ] && command -v bats &>/dev/null; then
bats tests/ || {
echo "❌ Tests BATS fallaron"
exit 1
}
echo "✅ Tests BATS: OK"
else
echo "⚠️ No se encontraron tests — saltando"
fi
fi
# Step 3: Build ───
echo ""
echo "🔨 [3/4] Build — Construyendo artefactos..."
if [ -f "Makefile" ]; then
make build || {
echo "❌ Build falló"
exit 1
}
echo "✅ Build: OK"
elif [ -f "Dockerfile" ]; then
docker build -t "${PROJECT_NAME}:${COMMIT_SHA::7}" . || {
echo "❌ Docker build falló"
exit 1
}
echo "✅ Docker build: OK"
else
echo "⚠️ No se encontró Makefile ni Dockerfile — saltando"
fi
# Step 4: Deploy (solo en main/master/develop) ───
echo ""
echo "🚀 [4/4] Deploy — Desplegando..."
case "$BRANCH" in
main|master)
echo " Rama principal detectada — desplegando a producción..."
if [ -f "deploy.sh" ]; then
bash deploy.sh production || {
echo "❌ Deploy a producción falló"
exit 1
}
echo "✅ Deploy a producción: OK"
fi
;;
develop)
echo " Rama develop detectada — desplegando a staging..."
if [ -f "deploy.sh" ]; then
bash deploy.sh staging || {
echo "❌ Deploy a staging falló"
exit 1
}
echo "✅ Deploy a staging: OK"
fi
;;
feature/*)
echo " Rama feature — deploy no automático"
;;
*)
echo " Rama '$BRANCH' — deploy no configurado"
;;
esac
echo ""
echo "════════════════════════════════════════════"
echo " ✅ Pipeline completado exitosamente"
echo "════════════════════════════════════════════"
Pre-commit lint hooks con shellcheck
#!/bin/bash
# — Hook especializado para linting
set -euo pipefail
echo "🔍 Verificando scripts Bash..."
# scripts Bash staged (añadidos o modificados)
staged_scripts=$(git diff --cached --name-only --diff-filter=ACM -- '*.sh' ':!node_modules/*' ':!vendor/*')
if [ -z "$staged_scripts" ]; then
echo " No hay scripts Bash en el commit"
exit 0
fi
errors=0
while IFS= read -r script; do
if [ ! -f "$script" ]; then
continue
fi
# Verificar sintaxis con bash -n
if ! bash -n "$script" 2>/dev/null; then
echo "❌ Error de sintaxis: $script"
errors=$((errors + 1))
continue
fi
# Verificar con shellcheck si está disponible
if command -v shellcheck &>/dev/null; then
if ! shellcheck -x -s bash "$script"; then
echo "⚠️ Advertencia de shellcheck: $script"
errors=$((errors + 1))
else
echo "✅ $script — OK"
fi
fi
done <<< "$staged_scripts"
# otros archivos con shebang bash
other_scripts=$(git diff --cached --name-only --diff-filter=ACM | while IFS= read -r f; do
[ -f "$f" ] && head -1 "$f" | grep -q '^#!/bin/bash' && echo "$f"
done)
while IFS= read -r script; do
[ -z "$script" ] && continue
echo " ${script}: tiene shebang bash"
done <<< "$other_scripts"
if [ "$errors" -gt 0 ]; then
echo "❌ $errors error(es) de linting — commit cancelado"
exit 1
fi
echo "✅ Linting Bash completado"
Variables de entorno en CI
En entornos CI, las variables de entorno reemplazan la interacción manual. Aquí hay un patrón para manejarlas correctamente:
#!/bin/bash
# seguro de variables de entorno CI
# por defecto para desarrollo local
export CI=${CI:-false}
export GITHUB_ACTIONS=${GITHUB_ACTIONS:-false}
export GITLAB_CI=${GITLAB_CI:-false}
# con defaults seguros
export DEPLOY_ENV=${DEPLOY_ENV:-development}
export LOG_LEVEL=${LOG_LEVEL:-info}
export TIMEOUT=${TIMEOUT:-30}
# — solo disponibles en CI
if [ "$CI" = "true" ]; then
# Estas variables vienen del CI, no del script
: "${DOCKER_USERNAME:?DOCKER_USERNAME no está definida en CI}"
: "${DOCKER_PASSWORD:?DOCKER_PASSWORD no está definida en CI}"
: "${DEPLOY_KEY:?DEPLOY_KEY no está definida en CI}"
echo "🔐 CI detectado — credenciales verificadas"
else
echo "💻 Entorno local — usando defaults"
fi
# helper para obtener variables con fallback
get_env() {
local var="$1"
local default="${2:-}"
echo "${!var:-$default}"
}
#
db_host=$(get_env "DB_HOST" "localhost")
db_port=$(get_env "DB_PORT" "5432")
echo "Conectando a $db_host:$db_port..."
Funciones profesionales (4+)
Funciones reutilizables para tu biblioteca personal:
#
# profesionales — Automatización Git
#
# — Version bump automático
# git_auto_version [major|minor|patch]
git_auto_version() {
local bump="${1:-patch}"
local last_tag=$(git tag --sort=-creatordate | head -1)
if [ -z "$last_tag" ]; then
new_version="v0.1.0"
else
local version="${last_tag#v}"
IFS='.' read -r major minor patch <<< "$version"
case "$bump" in
major) ((major++)); minor=0; patch=0 ;;
minor) ((minor++)); patch=0 ;;
patch) ((patch++)) ;;
*) echo "❌ Uso: git_auto_version {major|minor|patch}"; return 1 ;;
esac
new_version="v${major}.${minor}.${patch}"
fi
git tag -a "$new_version" -m "Release $new_version"
echo "✅ Tag $new_version creado"
echo "¿Hacer push? (s/N): " && read -r r
[[ "$r" = "s" ]] && git push origin "$new_version"
}
# — Generar changelog markdown entre tags
# git_changelog [desde_tag] [hasta_tag]
git_changelog() {
local desde="${1:-$(git tag --sort=-creatordate | tail -1)}"
local hasta="${2:-HEAD}"
local rango="${desde:+${desde}..}${hasta}"
echo "# Changelog"
echo "## [$hasta] - $(date +%Y-%m-%d)"
echo ""
git log "$rango" --format="- %s" --reverse 2>/dev/null | while IFS= read -r line; do
echo "$line"
done
[ -z "$desde" ] && return 0
echo ""
echo "---"
git log "$rango" --format="%h | %an | %ar | %s" --reverse 2>/dev/null
}
# — Limpiar ramas fusionadas (local y remoto)
# git_cleanup_branches [main|master]
git_cleanup_branches() {
local base="${1:-main}"
echo "🧹 Ramas locales fusionadas en $base:"
local branches
branches=$(git branch --merged "$base" | grep -vE "^\*|$base|master|develop" | tr -d ' ')
if [ -z "$branches" ]; then
echo " (ninguna)"
return 0
fi
echo "$branches" | sed 's/^/ /'
echo "¿Eliminar? (s/N): " && read -r r
[[ "$r" != "s" ]] && return 0
echo "$branches" | xargs -r git branch -d
echo "✅ Ramas locales eliminadas"
# Ramas remotas huérfanas
echo ""
echo "¿Eliminar ramas remotas sin tracking local? (s/N): " && read -r r
[[ "$r" != "s" ]] && return 0
git branch -r | grep -vE "origin/($base|master|develop|HEAD)" | while IFS= read -r remote_branch; do
local name="${remote_branch#origin/}"
if ! git branch | grep -qE "^\*?\s*$name$"; then
git push origin --delete "$name" 2>/dev/null && echo " Eliminada: $remote_branch"
fi
done
}
# — Despliegue automático (pull + build + restart)
# git_auto_deploy <directorio> <rama> <servicio>
git_auto_deploy() {
local dir="${1:?Uso: git_auto_deploy <dir> <rama> <servicio>}"
local branch="${2:?Falta rama}"
local service="${3:?Falta servicio}"
cd "$dir"
# Verificar rama
local current
current=$(git branch --show-current)
[ "$current" != "$branch" ] && {
echo "❌ Rama $current ≠ $branch"; return 1
}
# Guardar hash y hacer pull
local old_hash
old_hash=$(git rev-parse HEAD)
git fetch origin "$branch"
git reset --hard "origin/$branch"
[ "$old_hash" = "$(git rev-parse HEAD)" ] && {
echo "✅ Sin cambios"; return 0
}
# Build si existe Makefile
[ -f "Makefile" ] && make build
# Dependencias
[ -f "package.json" ] && npm install --production
[ -f "requirements.txt" ] && pip install -r requirements.txt
# Reiniciar
if command -v systemctl &>/dev/null && [ -n "$service" ]; then
sudo systemctl restart "$service"
fi
echo "✅ Deploy completado: $(git log --oneline "$old_hash..HEAD" | wc -l) commits"
}
Script completo: git_automation.sh
Un gestor de automatización Git todo-en-uno, en ~120 líneas:
#!/bin/bash
# — Automatización profesional de Git
# ./git_automation.sh <comando> [args]
# version|changelog|cleanup|deploy|status|backup|bisect|worktree|submodule|install-hooks
set -euo pipefail
GRN='\033[0;32m'; YLW='\033[1;33m'; RED='\033[0;31m'; BLU='\033[0;34m'; NC='\033[0m'
ok() { echo -e "${GRN}✓${NC} $*"; }
warn() { echo -e "${YLW}⚠${NC} $*" >&2; }
err() { echo -e "${RED}✗${NC} $*" >&2; }
info() { echo -e "${BLU}ℹ${NC} $*"; }
die() { err "$*"; exit 1; }
# Asegurar que estamos en un repositorio Git ──
ensure_git_repo() {
git rev-parse --git-dir &>/dev/null || die "No estás en un repositorio Git"
}
# version — Version bump automático ──
cmd_version() {
ensure_git_repo
local bump="${1:-patch}"
local last_tag; last_tag=$(git tag --sort=-creatordate | head -1)
local new_version
if [ -z "$last_tag" ]; then
new_version="v0.1.0"
else
local ver="${last_tag#v}"; IFS='.' read -r major minor patch <<< "$ver"
case "$bump" in
major) ((major++)); minor=0; patch=0 ;;
minor) ((minor++)); patch=0 ;;
patch) ((patch++)) ;;
*) die "Uso: version {major|minor|patch}" ;;
esac
new_version="v${major}.${minor}.${patch}"
fi
git tag -a "$new_version" -m "Release $new_version"
ok "Tag creado: $new_version"
echo -n "¿Push? (s/N): "; read -r r
[[ "$r" =~ ^[sS]$ ]] && git push origin "$new_version" && ok "Push de $new_version"
}
# changelog — Generar changelog ──
cmd_changelog() {
ensure_git_repo
local desde="${1:-}"
local hasta="${2:-HEAD}"
[ -z "$desde" ] && desde=$(git tag --sort=-creatordate | tail -1)
local rango="${desde:+${desde}..}${hasta}"
echo "# Changelog"
echo "## [$hasta] - $(date +%Y-%m-%d)"
echo ""
echo "### Cambios desde ${desde:-(inicio)}:"
git log "$rango" --format="- %s" --reverse 2>/dev/null || warn "Rango vacío: $rango"
echo ""
echo "### Detalle de commits:"
git log "$rango" --format="%h | %an | %ar | %s" --reverse 2>/dev/null || true
}
# cleanup — Limpiar ramas fusionadas ──
cmd_cleanup() {
ensure_git_repo
local base="${1:-main}"
local branches
branches=$(git branch --merged "$base" | grep -vE "^\*|$base|master|develop" | tr -d ' ')
if [ -z "$branches" ]; then
info "No hay ramas fusionadas para limpiar"
return 0
fi
warn "Ramas fusionadas en $base:"
echo "$branches" | sed 's/^/ /'
echo -n "¿Eliminar locales? (s/N): "; read -r r
[[ "$r" =~ ^[sS]$ ]] && echo "$branches" | xargs -r git branch -d && ok "Ramas locales eliminadas"
echo -n "¿Eliminar remotas huérfanas? (s/N): "; read -r r
[[ "$r" =~ ^[sS]$ ]] || return 0
git branch -r | grep -vE "origin/($base|master|develop|HEAD)" | while IFS= read -r rb; do
local name="${rb#origin/}"
git branch | grep -qE "^\*?\s*$name$" && continue
git push origin --delete "$name" 2>/dev/null && ok "Eliminada remota: $rb" || warn "No se pudo eliminar $rb"
done
}
# status — Estado completo del repo ──
cmd_status() {
ensure_git_repo
echo "═══ ESTADO DEL REPOSITORIO ═══"
local branch; branch=$(git branch --show-current)
echo "Rama: $branch"
# Sucio?
if [ -n "$(git status --porcelain)" ]; then
warn "Working tree: SUCIO"
git status --short | head -10
else
ok "Working tree: limpio"
fi
# Ahead/Behind
local remote_branch="origin/${branch}"
if git rev-parse "$remote_branch" &>/dev/null; then
local ahead; ahead=$(git rev-list --count "$remote_branch..HEAD" 2>/dev/null || echo 0)
local behind; behind=$(git rev-list --count "HEAD..$remote_branch" 2>/dev/null || echo 0)
echo "Ahead: $ahead commits"
echo "Behind: $behind commits"
fi
# Último tag
local last_tag; last_tag=$(git tag --sort=-creatordate | head -1)
echo "Último tag: ${last_tag:-(ninguno)}"
# Tamaño del .git
local size; size=$(du -sh .git 2>/dev/null | cut -f1)
echo "Tamaño .git: ${size:-desconocido}"
# Integridad
if git fsck --no-dangling 2>&1 | grep -qE '(error|missing)'; then
warn "⚠️ Problemas de integridad — ejecuta 'git fsck'"
fi
}
# deploy — Auto-deploy ──
cmd_deploy() {
ensure_git_repo
local branch="${1:-main}"
local service="${2:-}"
local current; current=$(git branch --show-current)
die_if_not_branch() { [ "$current" = "$1" ] || die "Rama actual: $current (se requiere: $1)"; }
die_if_not_branch "$branch"
local old_hash; old_hash=$(git rev-parse HEAD)
info "Haciendo pull de $branch..."
git fetch origin "$branch" && git reset --hard "origin/$branch"
local new_hash; new_hash=$(git rev-parse HEAD)
[ "$old_hash" = "$new_hash" ] && { ok "Sin cambios nuevos"; return 0; }
git log "$old_hash..$new_hash" --oneline --reverse | sed 's/^/ /'
[ -f "Makefile" ] && { info "make build..."; make build; }
[ -f "package.json" ] && { info "npm install..."; npm install --production; }
[ -f "requirements.txt" ] && { info "pip install..."; pip install -r requirements.txt; }
if [ -n "$service" ] && command -v systemctl &>/dev/null; then
info "Reiniciando $service..."
sudo systemctl restart "$service"
fi
ok "Deploy completado en $branch"
}
# backup — Crear bundle de backup ──
cmd_backup() {
ensure_git_repo
local backup_dir="${1:-/tmp/git-backups}"
mkdir -p "$backup_dir"
local name; name=$(basename "$(pwd)")
local file="${backup_dir}/${name}_$(date +%Y%m%d_%H%M%S).bundle"
info "Creando bundle: $file"
git bundle create "$file" --all || die "Error creando bundle"
git bundle verify "$file" &>/dev/null && ok "Bundle verificado" || die "Bundle corrupto"
gzip "$file" && ok "Comprimido: ${file}.gz"
ls -t "$backup_dir"/*.bundle.gz 2>/dev/null | tail -n +8 | xargs -r rm
info "Últimos 7 backups conservados"
}
# install-hooks — Instalar hooks predeterminados ──
cmd_install_hooks() {
ensure_git_repo
local hook_dir
hook_dir=$(git rev-parse --git-dir)/hooks
info "Instalando hooks en $hook_dir/..."
# pre-commit
cat > "$hook_dir/pre-commit" << 'HOOK'
#!/bin/bash
set -euo pipefail
echo "🔍 pre-commit: validando..."
if git diff --cached -U0 | grep -Pi '(password|secret|api.?key|token)\s*[:=]\s*["'"'"']?[^"'"'"'\s]{8,}' > /dev/null; then
echo "❌ Posible credencial en el diff"; exit 1
fi
if command -v shellcheck &>/dev/null; then
git diff --cached --name-only --diff-filter=ACM -- '*.sh' | while IFS= read -r f; do
[ -f "$f" ] && shellcheck -x "$f" || exit 1
done
fi
HOOK
chmod +x "$hook_dir/pre-commit"
ok "pre-commit instalado"
# pre-push
cat > "$hook_dir/pre-push" << 'HOOK'
#!/bin/bash
set -euo pipefail
branch=$(git branch --show-current)
case "$branch" in main|master|develop)
echo "⚠️ Push a $branch — confirma (s/N): "; read -r r
[ "$r" = "s" ] || exit 1
;;
esac
HOOK
chmod +x "$hook_dir/pre-push"
ok "pre-push instalado"
# post-commit
cat > "$hook_dir/post-commit" << 'HOOK'
#!/bin/bash
echo "[$(date '+%Y-%m-%d %H:%M:%S')] COMMIT $(git rev-parse --short HEAD): $(git log -1 --format='%s')" >> "$HOME/.git_commit_log"
HOOK
chmod +x "$hook_dir/post-commit"
ok "post-commit instalado"
ok "Hooks instalados. Git ahora ejecutará validaciones automáticas."
}
# bisect — Ejecutar bisect automatizado ──
cmd_bisect() {
ensure_git_repo
local test_script="${1:?Uso: bisect <script_test> [commit_bueno]}"
local good_commit="${2:-}"
[ -f "$test_script" ] || die "Script de test no encontrado: $test_script"
[ -x "$test_script" ] || die "Script no ejecutable: haz chmod +x"
git bisect start
git bisect bad HEAD
if [ -n "$good_commit" ]; then
git bisect good "$good_commit"
else
local tag; tag=$(git tag --sort=-creatordate | head -1)
[ -n "$tag" ] && git bisect good "$tag" || git bisect good HEAD~50 2>/dev/null || die "No se encontró commit bueno"
fi
info "Ejecutando bisect con: $test_script"
git bisect run bash "$test_script"
local result_sha; result_sha=$(git rev-parse HEAD)
local result_msg; result_msg=$(git log -1 --oneline "$result_sha")
info "Resultado del bisect:"
info "$result_msg"
git bisect reset
}
# Main ──
main() {
local cmd="${1:-help}"; shift 2>/dev/null || true
case "$cmd" in
version|changelog|cleanup|status|deploy|backup|install-hooks|bisect)
"cmd_${cmd}" "$@"
;;
help|*)
echo "╔══════════════════════════════════════════╗"
echo "║ git_automation.sh — Automatización Git ║"
echo "╚══════════════════════════════════════════╝"
echo "Uso: $0 <comando> [args]"
echo ""
echo " version [major|minor|patch] — Version bump + tag"
echo " changelog [desde] [hasta] — Generar changelog"
echo " cleanup [rama_base] — Limpiar ramas fusionadas"
echo " status — Estado completo del repo"
echo " deploy [rama] [servicio] — Auto-deploy (pull+build+restart)"
echo " backup [directorio] — Backup con git bundle"
echo " install-hooks — Instalar hooks predeterminados"
echo " bisect <script> [bueno] — Búsqueda binaria automatizada"
echo ""
echo "Ej: $0 version patch"
echo " $0 changelog v1.0.0 HEAD"
echo " $0 deploy main mi-app"
echo " $0 install-hooks"
;;
esac
}
main "$@"
Cómo usarlo:
chmod +x git_automation.sh
# bump (patch, minor, major)
./git_automation.sh version patch
./git_automation.sh version minor
./git_automation.sh version major
# changelog desde el último tag
./git_automation.sh changelog
# completo del repositorio
./git_automation.sh status
# ramas fusionadas
./git_automation.sh cleanup main
# en servidor
./git_automation.sh deploy main mi-app
# con git bundle
./git_automation.sh backup /backups/git
# hooks en el repositorio actual
./git_automation.sh install-hooks
# binaria para encontrar bug
./git_automation.sh bisect tests/test_login.sh
Errores comunes (10)
1: Hook no ejecutable
# El hook no tiene permiso de ejecución
# existe pero:
ls -la .git/hooks/pre-commit
# (sin 'x')
# Hacerlo ejecutable
chmod +x .git/hooks/pre-commit
# (con 'x')
2: Hook sale con código incorrecto
# El hook imprime error pero sale con 0
#
grep -q "BUG" src/ && echo "Error encontrado"
# con 0 aunque grep encontró error porque no hay exit 1!
# Forzar código de salida correcto
grep -q "BUG" src/ || exit 0 # Si no encuentra, ok
echo "Error: se encontró BUG"
exit 1
3: Git status –porcelain en directorio vacío
# Asumir que git status --porcelain siempre funciona
if [ "$(git status --porcelain)" != "" ]; then
echo "Hay cambios"
fi
# no hay cambios, el comando devuelve STRING VACÍO pero exit 0
# Verificar de forma robusta
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
echo "Hay cambios"
fi
# O usar diff-index (más eficiente)
if ! git diff-index --quiet HEAD --; then
echo "Hay cambios sin commitear"
fi
4: Push de tags sin actualizar ramas
# Subir tags sin subir las ramas asociadas
git tag v1.0.0
git push origin v1.0.0
# rama con el código de v1.0.0 no está en el remoto
# Subir rama y tag juntos
git push origin main
git push origin v1.0.0
# O subir todo de una vez
git push origin main --tags
5: Olvidar que los hooks no se versionan
# Clonas el repo en otro lado y los hooks no están
git clone git@server:proyecto.git
# solo tiene los hooks de ejemplo (terminados en .sample)
# Solución 1: Usar core.hooksPath (ruta compartida)
git config core.hooksPath .githooks
# .githooks/pre-commit (SÍ se versiona)
# Solución 2: Script de bootstrap
# scripts/setup-hooks.sh que copia los hooks
6: git log –format con espacios en el mensaje
# Parsear con for/while ingenuo
for commit in $(git log --format="%h %s"); do
echo "SHA: $commit"
done
# mensaje tiene espacios, for rompe todo!
# Usar delimitador e IFS
git log --format="%h|%s" | while IFS='|' read -r sha msg; do
echo "SHA: $sha | Mensaje: $msg"
done
# O con read -r -d
git log --format="%h%n%s%n---SEPARADOR---%n" | while IFS= read -r sha; do
IFS= read -r msg
IFS= read -r sep
echo "SHA: $sha | Mensaje: $msg"
done
7: Asumir que la rama remota existe
# Calcular ahead/behind sin verificar que la remota existe
ahead=$(git rev-list --count origin/main..HEAD 2>/dev/null)
# origin/main no existe, devuelve 0 sin error
# Verificar que la referencia remota existe
if git rev-parse --verify "origin/main" &>/dev/null; then
ahead=$(git rev-list --count origin/main..HEAD)
behind=$(git rev-list --count HEAD..origin/main)
else
echo "⚠️ origin/main no existe (primera vez?)"
ahead=0; behind=0
fi
8: git bisect reset olvidado
# Bisect se queda en medio de la búsqueda
git bisect bad HEAD
git bisect good v1.0.0
git bisect run ./test.sh
# el script falla internamente, git bisect queda activo
# el repo está en estado DETACHED HEAD
# Siempre envolver en try/finally
cleanup() { git bisect reset 2>/dev/null || true; }
trap cleanup EXIT
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run ./test.sh
# se ejecuta siempre aunque falle
9: Trabajar con submodules sin –recursive
# Clonar sin --recursive, submodules vacíos
git clone git@server:proyecto-con-submodulos.git
cd proyecto-con-submodulos
ls libs/mi-libreria/ # ¡Directorio vacío!
# Clonar con submodules
git clone --recursive git@server:proyecto-con-submodulos.git
# Si ya clonaste, inicializar manualmente
git submodule init
git submodule update --recursive
# Mejor: alias para olvidarte del problema
git config --global alias.clone-full 'clone --recursive'
10: Hacer commit tras pre-commit sin staged changes
# El pre-commit modifica archivos pero no los añade al stage
#
terraform fmt # Formatea .tf files
# cambio queda sin stage → commit con código sin formatear
# Añadir los cambios formateados al stage
# (avanzado)
files_changed=false
git diff --cached --name-only --diff-filter=ACM -- '*.tf' | while IFS= read -r f; do
terraform fmt "$f"
git add "$f"
files_changed=true
done
Conclusiones
| Concepto | Comando / Script | Propósito |
|---|---|---|
| pre-commit hook | .git/hooks/pre-commit | Validar antes de crear el commit (linters, credenciales) |
| post-commit hook | .git/hooks/post-commit | Reaccionar tras el commit (notificaciones, logs) |
| pre-push hook | .git/hooks/pre-push | Validar antes de enviar al remoto (tests, protección) |
| post-merge hook | .git/hooks/post-merge | Reaccionar tras fusionar (deps, builds) |
| pre-receive hook | hooks/pre-receive (servidor) | Validar en el servidor antes de aceptar push |
| git log –format | %h, %an, %ar, %s, %ai | Formato personalizado de salida del log |
| git log –oneline –graph | git log --oneline --graph --all | Visualizar historial con ramas |
| git status –porcelain | git status --porcelain | Estado estable y parseable del working tree |
| git diff-index –quiet | git diff-index --quiet HEAD -- | Verificar si hay cambios (rápido) |
| git rev-list –count | rev-list --count A..B | Contar commits ahead/behind |
| git tag -a | git tag -a v1.0.0 -m "..." | Tag anotado (recomendado para releases) |
| git push –tags | git push origin --tags | Subir todos los tags al remoto |
| Version bump | git_auto_version patch | Incrementar versión y crear tag automáticamente |
| Changelog | git_changelog v1.0.0 HEAD | Generar CHANGELOG.md desde git log |
| Branch cleanup | git_cleanup_branches main | Eliminar ramas locales y remotas fusionadas |
| Auto-deploy | git_auto_deploy dir main service | Pull + build + restart automatizado |
| git bisect run | git bisect run ./test.sh | Búsqueda binaria automatizada |
| git bundle | git bundle create repo.bundle --all | Backup completo del repositorio en un archivo |
| git worktree | git worktree add ../dir rama | Múltiples ramas checkout al mismo tiempo |
| git submodule | git submodule update --recursive | Gestión de submódulos |
| Shellcheck hook | shellcheck -x script.sh | Linter para scripts Bash en pre-commit |
| CI/CD pipeline | ci_pipeline.sh | Pipeline portable: lint → test → build → deploy |
| core.hooksPath | git config core.hooksPath .githooks | Hooks compartidos y versionables |
| git fsck | git fsck --no-dangling | Verificar integridad del repositorio |
| git gc | git gc --auto | Limpiar objetos no referenciados |
Mas información,
- Git Hooks — Atlassian Guide — Guía completa de hooks con ejemplos prácticos
- GitHub Git Cheat Sheet — Referencia rápida de comandos Git
- Conventional Commits — Especificación de mensajes de commit semánticos
- Semantic Versioning — Especificación de versionado semántico
- Git bisect run — Loren’s Blog — Ejemplos avanzados de bisect run
- Git Worktree Tutorial — Uso práctico de worktrees
- Pro Git Book (Scott Chacon) — El libro definitivo sobre Git (gratuito)
- Git Hooks — Customizing Git — Capítulo de hooks del Pro Git
- Shellcheck — Github — Repositorio oficial del linter
- GitHub Actions Workflow Syntax — Pipelines CI/CD con GitHub
- «Pro Git» de Scott Chacon y Ben Straub — La biblia de Git, completamente gratuita en git-scm.com
- «Git Pocket Guide» de Richard E. Silverman — Referencia compacta para el día a día
- «Bash Cookbook» de Carl Albing — Recetas que incluyen automatización con Git
- «Learn Git in a Month of Lunches» de Rick Umali — Enfoque práctico con scripts
- «The Linux Programming Interface» de Michael Kerrisk — Conceptos subyacentes de procesos y señales que explican cómo funcionan los hooks
- «Engineering DevOps» de Marc Hornbeek — Automatización de pipelines CI/CD desde la infraestructura