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.
| Objeto | Qué representa |
|---|---|
| Blob | El contenido de un fichero, sin nombre ni permisos |
| Tree | Un directorio: lista de nombres que apuntan a blobs y a otros trees |
| Commit | Una instantánea: apunta a un tree raíz, a su commit padre, y lleva autor, fecha y mensaje |
| Tag anotada | Un nombre permanente para un commit, con autor, mensaje y opcionalmente firma |
Dos ficheros idénticos comparten el mismo blob: se guarda una sola vez.
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 HEADLos tres árboles
Git trabaja con tres zonas, y casi todo comando mueve cambios entre ellas.
git add
git commit
git restore y git reset mueven los cambios en sentido contrario.
| Zona | Qué contiene | Se actualiza con |
|---|---|---|
| Directorio de trabajo | Los ficheros tal como se editan | El editor |
| Índice o staging | La propuesta del próximo commit | git add |
| HEAD (repositorio) | El último commit de la rama | git 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 #214La 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.
| Tipo | Uso | Efecto en SemVer |
|---|---|---|
feat | Nueva funcionalidad | Sube la versión menor |
fix | Corrección de un error | Sube el parche |
refactor | Reestructuración sin cambio de comportamiento | Ninguno |
perf | Mejora de rendimiento | Parche |
test, docs, chore, ci, build | Pruebas, documentación, mantenimiento, integración, construcción | Ninguno |
feat! o pie BREAKING CHANGE: | Cambio incompatible | Sube 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.
Si algo falla, se corrige en la misma rama.
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.
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.
Los cambios solo fluyen hacia adelante: nunca se corrige directamente en producción.
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.
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.
| Estrategia | Encaja cuando | Coste |
|---|---|---|
| GitHub Flow | Se despliega a menudo y solo hay una versión en producción | Exige buenas pruebas automáticas |
| Git Flow | Versiones programadas o varias versiones mantenidas | Más ramas, más fusiones, integración tardía |
| GitLab Flow | Hay entornos intermedios o versiones soportadas | Disciplina de promoción entre ramas |
| Trunk-based | Equipo maduro con CI sólida y despliegue frecuente | Requiere 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.
Dos formas de integrar
Con squash se pierde el detalle intermedio, pero main queda con un commit por cambio.
# 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étodo | Historia resultante | Ventaja | Inconveniente |
|---|---|---|---|
| Merge | Con bifurcaciones y commits de unión | No reescribe nada, conserva el contexto | Grafo más enrevesado |
| Rebase | Lineal | Fácil de leer y de bisecar | Reescribe commits: peligroso en ramas compartidas |
| Squash | Un commit por cambio en main | Main limpio, un commit revertible | Se 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.
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/impuestosgit 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 dudasPara 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.
En paralelo
Se repite hasta que todo está en verde y aprobado.
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
| Aspecto | Preguntas |
|---|---|
| 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.
- 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 @documentacionCommits 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 -1Hooks: 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-conflictLos 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
| Situación | Comando | Notas |
|---|---|---|
| Descartar cambios de un fichero | git restore fichero | Irreversible para lo no confirmado |
| Sacar un fichero del índice | git restore --staged fichero | Conserva los cambios en el directorio |
| Corregir el último commit | git commit --amend | Reescribe el commit: solo si no se ha publicado |
| Deshacer commits locales | git reset --soft HEAD~1 | Mantiene los cambios en el índice |
| Descartar commits locales y cambios | git reset --hard HEAD~1 | Destructivo en el directorio de trabajo |
| Deshacer un commit ya publicado | git revert hash | Crea un commit inverso, no reescribe |
| Llevar un commit a otra rama | git cherry-pick hash | Copia el cambio, no el historial |
| Guardar trabajo a medias | git stash push -m "descripción" | Se recupera con git stash pop |
| Encontrar el commit que rompió algo | git bisect | Bú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 resetCon 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
*.pemLos 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
| Criterio | Monorepo | Multirepo |
|---|---|---|
| Cambios que cruzan componentes | Un solo commit y una sola revisión | Varios PR coordinados y versiones intermedias |
| Compartir código | Directo | Mediante paquetes versionados |
| Permisos y propiedad | Más difícil de acotar (ayuda CODEOWNERS) | Naturales por repositorio |
| Integración continua | Necesita construir solo lo afectado | Sencilla por repositorio |
| Herramientas | Requiere soporte de caché y grafo de tareas | Estándar |
| Escala | Exige cuidado con el tamaño del repositorio | Má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.