Publicado en

Git: merge, rebase y pull requests

Comparación entre git merge que produce un historial bifurcado y git rebase que produce un historial lineal, sobre fondo oscuro

Dos desarrolladores trabajan en ramas distintas durante una semana. Cuando intentan integrar su trabajo, sus historiales han divergido. Hay dos formas de reunirlos: merge y rebase. Ambas consiguen el mismo resultado final, pero el historial que dejan es radicalmente diferente, y eso importa.

git merge

git merge toma dos ramas y crea un nuevo commit que une sus historiales. Es la opción más segura porque no modifica ningún commit existente.

# Estando en main, fusionar la rama feature
git switch main
git merge feature/nueva-funcionalidad
 
# Merge con commit explícito aunque sea posible fast-forward
git merge --no-ff feature/nueva-funcionalidad

Si las dos ramas han avanzado en paralelo, Git crea un merge commit: un commit con dos padres que representa la unión. El historial refleja exactamente lo que ocurrió, con la bifurcación visible.

git rebase

git rebase toma los commits de una rama y los reaplica sobre otra, como si la rama hubiera empezado desde ahí. El resultado es un historial completamente lineal, sin commits de merge.

# Desde la rama feature, rebasear sobre main
git switch feature/nueva-funcionalidad
git rebase main
 
# Rebase interactivo: reordenar, squash o editar los últimos 3 commits
git rebase -i HEAD~3
Diagrama comparando git merge y git rebase: merge conserva el historial bifurcado con un merge commit, rebase reescribe los commits generando un historial lineal

La diferencia clave: rebase reescribe el historial. Los commits de la feature obtienen nuevos hashes. Eso está bien en ramas locales privadas, pero es peligroso en ramas compartidas: si otros desarrolladores tienen esos commits, sus repositorios quedan en estado inconsistente.

La regla de oro: nunca hagas rebase de commits que ya están en el remoto y que otros puedan tener. Si necesitas actualizar una rama compartida, usa merge.

Cuándo usar cada uno

  • merge: para integrar ramas en main o develop. En ramas compartidas. Cuando quieres preservar el contexto de cuándo ocurrió el trabajo paralelo.
  • rebase: para actualizar tu rama feature local con los últimos cambios de main antes de abrir un PR. Para limpiar el historial de una rama antes de fusionarla.

Resolución de conflictos

Un conflicto ocurre cuando dos ramas han modificado las mismas líneas del mismo archivo de forma incompatible. Git no puede decidir qué versión mantener y te pide que lo resuelvas manualmente.

# Git marca el estado con conflicto
git status
# both modified: src/usuario.js
 
# El archivo tiene marcadores de conflicto:
# <<<<<<< HEAD
# código de tu rama actual
# =======
# código de la rama que estás fusionando
# >>>>>>> feature/nueva-funcionalidad

El proceso de resolución siempre sigue los mismos tres pasos:

# 1. Editar el archivo: eliminar los marcadores y dejar el código correcto
#    (puede ser una versión, la otra, o una combinación de ambas)
 
# 2. Marcar el conflicto como resuelto
git add src/usuario.js
 
# 3. Continuar el merge o rebase
git merge --continue
git rebase --continue
 
# Para cancelar y volver al estado previo
git merge --abort
git rebase --abort

Herramientas como VS Code, IntelliJ o git mergetool muestran los conflictos de forma visual con botones para elegir qué versión conservar, lo que agiliza la resolución considerablemente.

Pull Requests

Los cinco pasos del flujo de un Pull Request: crear rama, commits y push, abrir PR, revisión de código con aprobación, y merge a main

Un Pull Request (o Merge Request en GitLab) es una propuesta formal de fusionar una rama en otra. No es una funcionalidad de Git en sí, sino una capa que añaden las plataformas como GitHub o GitLab para facilitar la revisión de código antes del merge.

# El flujo completo desde la terminal
git switch -c feature/nueva-funcionalidad
# … commits …
git push origin feature/nueva-funcionalidad
# → abrir el PR en la interfaz web de GitHub/GitLab

El PR muestra el diff de los cambios, permite añadir comentarios línea a línea, solicitar cambios y aprobar. Una vez aprobado, el merge se puede hacer de tres formas desde la plataforma:

  • Merge commit: preserva todos los commits de la feature más un commit de merge. El historial muestra exactamente qué venía de qué rama.
  • Squash and merge: comprime todos los commits de la feature en uno solo antes de hacer el merge. El historial de main queda limpio con un commit por feature.
  • Rebase and merge: reaplica los commits de la feature sobre main sin crear un merge commit. Historial lineal.

Para proyectos con deploy continuo, squash and merge suele ser la mejor opción: cada feature queda como un commit atómico y bien descrito en main, fácil de revertir si es necesario.

Buenas prácticas en revisión de código

  • Un PR debe hacer una sola cosa. Mezclar refactor con nuevas funcionalidades dificulta la revisión.
  • Limpia el historial de la rama antes de abrir el PR con git rebase -i: agrupa commits de «fix» con su commit principal, corrige mensajes genéricos.
  • La descripción del PR debe explicar por qué se hace el cambio, no solo qué hace. El código ya muestra el qué.
  • Responde a todos los comentarios antes de solicitar una nueva revisión.

Con las ramas, los flujos de trabajo y la colaboración cubiertos, el siguiente paso en DevOps es entender cómo se empaqueta una aplicación para que se ejecute de forma consistente en cualquier entorno. Eso es precisamente lo que resuelve un Dockerfile.