Saltar al contenido principal
Jordi Izquierdo Montalbán
Lo que practico

Fundamentos

Control de versiones

Una rama por tarea, commits claros y merge solo con las pruebas en verde.

Git por dentro: por qué funciona como funciona

La mayoría de los problemas con Git se vuelven sencillos cuando se entiende su modelo de datos. Git no guarda «diferencias» entre versiones: guarda instantáneas de todo el proyecto, y lo hace con cuatro tipos de objetos identificados por el hash de su contenido.

ObjetoQué representa
BlobEl contenido de un fichero, sin nombre ni permisos
TreeUn directorio: lista de nombres que apuntan a blobs y a otros trees
CommitUna instantánea: apunta a un tree raíz, a su commit padre, y lleva autor, fecha y mensaje
Tag anotadaUn nombre permanente para un commit, con autor, mensaje y opcionalmente firma
Git guarda contenido direccionado por hash: un commit apunta a un árbol y el árbol a los blobs.

Como el identificador depende del contenido, cambiar cualquier cosa de un commit (incluido su padre) cambia su hash. Por eso «reescribir la historia» en realidad crea commits nuevos y abandona los antiguos, y por eso un historial compartido no debe reescribirse.

Referencias y HEAD

Una rama no es una carpeta ni una copia: es un fichero diminuto con el hash de un commit. Al hacer un commit en la rama actual, ese puntero avanza. HEAD indica dónde estás: normalmente apunta a una rama, y cuando apunta directamente a un commit se dice que estás en HEAD desacoplado (detached HEAD), útil para inspeccionar pero peligroso para acumular trabajo sin rama.

git log --oneline --graph --all   # ver la historia como un grafo
git cat-file -p HEAD              # inspeccionar el commit actual
git rev-parse HEAD~2              # hash del abuelo de HEAD

Los tres árboles

Git trabaja con tres zonas, y casi todo comando mueve cambios entre ellas.

Cada comando mueve cambios entre el directorio de trabajo, el índice y el repositorio.
ZonaQué contieneSe actualiza con
Directorio de trabajoLos ficheros tal como se editanEl editor
Índice o stagingLa propuesta del próximo commitgit add
HEAD (repositorio)El último commit de la ramagit commit

El índice permite construir commits atómicos aunque se hayan tocado muchas cosas a la vez: git add -p deja elegir fragmento a fragmento qué entra en cada commit.

Commits que se pueden leer dentro de un año

Un commit es la unidad de la historia. Para que git log, git blame y git bisect sean útiles, cada commit debe ser atómico (un único cambio lógico), dejar el proyecto en un estado que compila y tener un mensaje que explique el porqué.

fix(carrito): evitar el doble envío del formulario
 
El botón seguía activo mientras la petición estaba en vuelo, lo que
creaba pedidos duplicados con conexiones lentas. Se desactiva al
enviar y se reactiva solo si la petición falla.
 
Closes #214

La primera línea (unos 50 caracteres, en imperativo) resume; el cuerpo, separado por una línea en blanco, explica la motivación y las alternativas descartadas.

Conventional Commits

Es una convención con formato tipo(ámbito): descripción que permite automatizar la generación del registro de cambios y el cálculo de la versión.

TipoUsoEfecto en SemVer
featNueva funcionalidadSube la versión menor
fixCorrección de un errorSube el parche
refactorReestructuración sin cambio de comportamientoNinguno
perfMejora de rendimientoParche
test, docs, chore, ci, buildPruebas, documentación, mantenimiento, integración, construcciónNinguno
feat! o pie BREAKING CHANGE:Cambio incompatibleSube la versión mayor

Estrategias de ramas

No existe una estrategia universalmente mejor: depende de la frecuencia de despliegue, del tamaño del equipo y de cuántas versiones hay que mantener a la vez.

GitHub Flow

Un modelo mínimo: main siempre es desplegable y todo cambio entra mediante una rama corta y un pull request.

Una rama corta por cambio, revisada antes de entrar en la rama principal.

Git Flow

Pensado para software con versiones programadas. Mantiene develop como rama de integración, ramas release/* para estabilizar y hotfix/* para urgencias en producción.

Ramas de larga duración para separar el desarrollo de lo que está en producción.

GitLab Flow

Parte de main e incorpora ramas de entorno (pre-production, production) o de versión (release/1.2). Los cambios se promocionan en un solo sentido, lo que simplifica saber qué hay en cada entorno.

Main integra y ramas de entorno o de versión reciben los cambios en orden.

Trunk-based development

Todo el equipo integra en una única rama varias veces al día, con ramas que duran horas. Lo no terminado se esconde tras feature flags. Exige integración continua sólida y revisión ágil.

Todos integran en la rama principal varias veces al día.

Ramas de versión (release branches)

Cuando hay que mantener varias versiones soportadas, se crea una rama por versión (release/2.4) y las correcciones se aplican en main y se llevan a las ramas de versión con git cherry-pick. Es compatible tanto con trunk-based como con GitLab Flow.

EstrategiaEncaja cuandoCoste
GitHub FlowSe despliega a menudo y solo hay una versión en producciónExige buenas pruebas automáticas
Git FlowVersiones programadas o varias versiones mantenidasMás ramas, más fusiones, integración tardía
GitLab FlowHay entornos intermedios o versiones soportadasDisciplina de promoción entre ramas
Trunk-basedEquipo maduro con CI sólida y despliegue frecuenteRequiere feature flags y revisiones rápidas

Nombres de ramas

Un prefijo por tipo de trabajo ayuda a filtrar y a automatizar: feature/exportar-pdf, fix/doble-envio, chore/actualizar-dependencias. Si el equipo usa un gestor de tareas, incluir su identificador facilita el rastreo.

Integrar cambios: merge, rebase y squash

Los tres integran trabajo, pero dejan una historia distinta.

Merge conserva la bifurcación con un commit de unión; rebase recoloca los commits para dejar una línea recta.
# Merge: conserva la bifurcación y crea un commit de unión
git switch main
git merge --no-ff feature/exportar-pdf
 
# Rebase: reaplica los commits de la rama sobre la punta de main
git switch feature/exportar-pdf
git rebase main
 
# Squash: condensa la rama en un solo commit en main
git merge --squash feature/exportar-pdf
git commit -m "feat: exportar el informe a PDF"
MétodoHistoria resultanteVentajaInconveniente
MergeCon bifurcaciones y commits de uniónNo reescribe nada, conserva el contextoGrafo más enrevesado
RebaseLinealFácil de leer y de bisecarReescribe commits: peligroso en ramas compartidas
SquashUn commit por cambio en mainMain limpio, un commit revertibleSe pierde el detalle de los pasos intermedios

La regla de oro del rebase: solo se reescribe lo que todavía no ha visto nadie más. Si hay que subir una rama reescrita, git push --force-with-lease es más seguro que --force, porque se niega si alguien ha subido algo entretanto.

Resolver conflictos

Un conflicto aparece cuando dos ramas modifican las mismas líneas y Git no puede decidir cuál prevalece. No es un fallo: es una pregunta que solo una persona puede contestar.

Un conflicto solo indica que dos cambios tocan las mismas líneas: la decisión es humana.

Git deja marcadores en el fichero. Entre el inicio y el separador está la versión actual y, entre el separador y el final, la entrante:

<<<<<<< HEAD
const iva = 0.21;
=======
const iva = 0.21; // tipo general
>>>>>>> feature/impuestos
git status                  # ficheros sin fusionar
# editar el fichero, quitar los marcadores y dejar el resultado correcto
git add ruta/al/fichero
git merge --continue        # o git rebase --continue
git merge --abort           # volver al estado previo si hay dudas

Para reducir conflictos: ramas cortas, integrar main con frecuencia, no reformatear ficheros enteros mezclado con cambios de lógica y activar git config rerere.enabled true, que recuerda cómo se resolvió un conflicto repetido.

Etiquetas y versionado semántico

Una etiqueta anotada marca un punto concreto de la historia, normalmente una versión publicada. Con SemVer el número MAYOR.MENOR.PARCHE comunica el impacto: mayor rompe compatibilidad, menor añade sin romper y parche corrige.

git tag -a v1.4.0 -m "Versión 1.4.0"
git push origin v1.4.0
git tag --list "v1.*"

Las etiquetas anotadas (con -a) llevan autor y mensaje; las ligeras son solo un puntero. Para versiones publicadas conviene siempre la anotada, o incluso firmada con -s.

Pull requests y revisión de código

Un pull request (o merge request) es la conversación sobre un cambio antes de integrarlo. Su valor depende de que sea pequeño y esté bien descrito.

El PR es el punto donde confluyen revisión humana y comprobaciones automáticas.

Qué incluir en la descripción

  • Qué cambia y por qué (enlace a la tarea o incidencia).
  • Cómo se ha probado y qué queda fuera de este cambio.
  • Capturas si hay cambios visuales y notas de migración si las hay.

Qué mira quien revisa

AspectoPreguntas
Corrección¿Hace lo que dice? ¿Qué pasa en los casos límite y en los fallos?
Diseño¿Encaja en la arquitectura? ¿Es la complejidad necesaria?
Pruebas¿Cubren el comportamiento nuevo y el error corregido?
Seguridad¿Valida entradas? ¿Expone datos o secretos?
Legibilidad¿Nombres claros? ¿Se entiende sin explicación oral?
Compatibilidad¿Rompe contratos, migraciones o configuración existente?

Sobre el tamaño: un cambio que se puede revisar con atención en menos de media hora recibe revisiones útiles; uno enorme recibe «parece bien». Si un PR crece demasiado, se divide en cambios independientes que puedan integrarse por separado.

Proteger la rama principal

Las buenas prácticas que dependen de la memoria de las personas acaban incumpliéndose. Se configuran en la plataforma.

Las reglas se aplican en la plataforma, no se dejan a la buena voluntad de cada persona.
  • Sin push directo a main; todo entra por pull request.
  • Checks obligatorios: compilación, pruebas y análisis estático en verde.
  • Revisión obligatoria de al menos una persona, y que se invalide si se suben cambios nuevos.
  • Prohibir el push forzado y el borrado de la rama protegida.
  • Historia lineal si el equipo prefiere rebase o squash.

Un fichero CODEOWNERS asigna automáticamente revisores según la ruta:

# Cada ruta tiene responsables que deben aprobar sus cambios
/infra/            @equipo-plataforma
/src/pagos/        @equipo-pagos @seguridad
*.md               @documentacion

Commits firmados

Cualquiera puede escribir cualquier nombre en user.name. Firmar los commits con GPG, SSH o Sigstore demuestra que los hizo quien dice haberlos hecho.

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git log --show-signature -1

Hooks: automatizar antes de que llegue al repositorio

Los hooks son scripts que Git ejecuta en momentos concretos. El más usado es pre-commit, que corre formateadores, linters y detectores de secretos antes de aceptar el commit. Como no se versionan dentro de .git, se suelen gestionar con una herramienta como pre-commit, Husky o Lefthook.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: end-of-file-fixer
      - id: check-merge-conflict

Los hooks locales se pueden saltar con --no-verify, así que no sustituyen a las comprobaciones de la integración continua, que son la barrera real.

Recuperación: deshacer sin entrar en pánico

Casi nada se pierde en Git mientras exista en el reflog; lo importante es elegir la herramienta adecuada.
SituaciónComandoNotas
Descartar cambios de un ficherogit restore ficheroIrreversible para lo no confirmado
Sacar un fichero del índicegit restore --staged ficheroConserva los cambios en el directorio
Corregir el último commitgit commit --amendReescribe el commit: solo si no se ha publicado
Deshacer commits localesgit reset --soft HEAD~1Mantiene los cambios en el índice
Descartar commits locales y cambiosgit reset --hard HEAD~1Destructivo en el directorio de trabajo
Deshacer un commit ya publicadogit revert hashCrea un commit inverso, no reescribe
Llevar un commit a otra ramagit cherry-pick hashCopia el cambio, no el historial
Guardar trabajo a mediasgit stash push -m "descripción"Se recupera con git stash pop
Encontrar el commit que rompió algogit bisectBúsqueda binaria entre uno bueno y uno malo

revert frente a reset: revert añade historia y es seguro en ramas compartidas; reset mueve el puntero y reescribe, por lo que solo debe usarse en trabajo local.

El reflog

Git registra cada movimiento de HEAD durante un tiempo en el reflog, incluso commits que ninguna rama alcanza ya. Tras un reset --hard o un rebase equivocado suele ser posible volver:

git reflog                       # historial de movimientos de HEAD
git reset --hard HEAD@{2}        # volver a donde estaba hace dos movimientos
git switch -c rescate abc1234    # o crear una rama en un commit "perdido"

Bisect

git bisect start
git bisect bad                   # el commit actual falla
git bisect good v1.3.0           # esta versión funcionaba
# Git propone commits intermedios; se prueba y se marca good o bad
git bisect run pytest tests/test_pedidos.py   # o automatizarlo
git bisect reset

Con commits atómicos y que compilan, bisect encuentra el culpable en pocos pasos.

Secretos y ficheros que no deben estar

El fichero .gitignore evita que se añadan por accidente binarios generados, dependencias instaladas, configuraciones locales y credenciales. Un .gitignore no afecta a ficheros que ya están versionados.

node_modules/
.venv/
dist/
.env
.env.*
!.env.example
*.pem
Borrar el fichero no basta: el secreto sigue en el historial y en las copias ya clonadas.

Los secretos viven en variables de entorno o en un gestor de secretos, y en el repositorio solo se versiona un ejemplo sin valores (.env.example). Un escáner de secretos en pre-commit y en la integración continua cierra el ciclo.

Monorepo frente a multirepo

CriterioMonorepoMultirepo
Cambios que cruzan componentesUn solo commit y una sola revisiónVarios PR coordinados y versiones intermedias
Compartir códigoDirectoMediante paquetes versionados
Permisos y propiedadMás difícil de acotar (ayuda CODEOWNERS)Naturales por repositorio
Integración continuaNecesita construir solo lo afectadoSencilla por repositorio
HerramientasRequiere soporte de caché y grafo de tareasEstándar
EscalaExige cuidado con el tamaño del repositorioMás repositorios que gobernar

No hay respuesta correcta general: un monorepo favorece los cambios transversales y la coherencia; varios repositorios favorecen la autonomía y los límites claros entre equipos.

Lista de comprobación

  • La rama principal está protegida y solo se cambia mediante pull request.
  • Los commits son atómicos y el mensaje explica el porqué.
  • Se acuerda una única estrategia de integración (merge, rebase o squash).
  • Hay CODEOWNERS en las áreas sensibles.
  • Un escáner de secretos corre en local y en CI.
  • Las versiones publicadas llevan etiqueta anotada.
  • El equipo sabe usar reflog, revert y bisect.

Antipatrones

  • Ramas de larga duración que acumulan divergencia y acaban en una fusión dolorosa.
  • Commits «WIP» y «arreglos» que no dicen nada y dificultan bisect.
  • Mezclar en un commit formato, refactorización y lógica.
  • Push forzado sobre ramas compartidas.
  • Subir ficheros generados o dependencias instaladas.
  • Guardar secretos en el repositorio, aunque sea privado.
  • Pull requests gigantes que nadie revisa de verdad.
  • Resolver conflictos «aceptando lo mío» sin entender qué cambió la otra parte.