El comando git rebase tiene la reputación de ser una especie de magia negra de Git a la que los principiantes no deben acercarse, pero, en realidad, puede ponerle las cosas mucho más fáciles al equipo de desarrollo si lo usan con cuidado. En este artículo, comparamos el comando git rebase con el comando relacionado git merge e identificamos las oportunidades potenciales de incorporar la fusión mediante cambio de base al flujo de trabajo habitual de Git.

Resumen de conceptos

Lo primero que hay que entender del comando git rebase es que soluciona el mismo problema que git merge. Ambos comandos están diseñados para integrar cambios de una rama a otra, pero lo hacen de forma muy distinta.

Piensa en lo que ocurre cuando empiezas a trabajar en una nueva función en una rama específica y, luego, otro miembro del equipo actualiza la rama main con nuevas confirmaciones. El resultado es un historial bifurcado, lo que debería resultarle familiar a cualquiera que haya usado Git como herramienta de colaboración.

Un historial de confirmaciones bifurcado

Digamos ahora que las nuevas confirmaciones de la rama main son relevantes para la función en la que estás trabajando. Para incorporar las nuevas confirmaciones a tu rama feature, tienes dos opciones: la fusión (merge) o la fusión mediante cambio de base (rebase).

La opción de fusión

La opción más sencilla es fusionar la rama main con la rama de función mediante algo similar a esto:

git checkout feature
git merge main

También puedes agrupar esto en una línea:

git merge feature main

Así se crea una "confirmación de fusión" en la rama feature que une los historiales de ambas ramas, lo que te proporciona una estructura de rama que tiene este aspecto:

Fusión de la rama master en la rama feature

La fusión está bien, porque es una operación no destructiva. Las ramas existentes no cambian en ningún aspecto. De este modo, se evitan todos los peligros potenciales de la reorganización (que se comentan más adelante).

Por otro lado, esto también significa que la rama feature tendrá una confirmación de fusión externa cada vez que necesites incorporar cambios de nivel superior. Si la rama main es muy activa, esto puede contaminar bastante el historial de tu rama de función. Sin bien es posible mitigar este problema con opciones avanzadas de git log, para otros desarrolladores puede resultar más difícil entender el historial del proyecto.

La opción de reorganización

Como alternativa a la fusión, puedes fusionar mediante cambio de base la rama feature en la rama main con los siguientes comandos:

git checkout feature
git rebase main

De este modo, la rama feature se mueve y empieza en el extremo de la rama main, y se incorporan de manera eficaz las nuevas confirmaciones en main. Pero, en lugar de usar una confirmación de fusión, la fusión mediante cambio de base reescribe el historial del proyecto creando nuevas confirmaciones para cada confirmación en la rama original.

Reorganización de la rama feature en la rama master

La principal ventaja es que el historial del proyecto queda mucho más limpio. En primer lugar, se eliminan los commits de fusión innecesarios que requiere el comando git merge. En segundo lugar, como puedes ver en el diagrama anterior, la reorganización genera un historial de proyecto perfectamente lineal: se puede ir desde el extremo de la rama feature hasta el inicio del proyecto sin encontrar ninguna bifurcación. Así es más fácil navegar por el proyecto con comandos como git log, git bisect y gitk.

Sin embargo, este prístino historial de commits tiene dos desventajas: la seguridad y la trazabilidad. Si no sigues la regla de oro de la reorganización, reescribir el historial del proyecto puede tener consecuencias catastróficas para el workflow de colaboración. Y, aunque menos importante, también se pierde el contexto que proporciona el commit de fusión: no se puede ver cuándo se han incorporado los cambios de nivel superior a la funcionalidad.

Reorganización interactiva

La fusión mediante cambio de base interactiva te brinda la oportunidad de alterar las confirmaciones al moverlas a la nueva rama. Es aún más potente que una fusión mediante cambio de base automatizada porque te da control absoluto sobre el historial de confirmaciones de la rama. Habitualmente, se usa para organizar un historial desordenado antes de fusionar una rama de función con la rama main.

Para empezar una sesión de fusión mediante cambio de base interactiva, añade la opción i al comando git rebase:

git checkout feature
git rebase -i main

Así se abre un editor de texto en el que se enumeran todas las confirmaciones que se van a mover:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Esta lista define exactamente el aspecto que tendrá la rama cuando se aplique la fusión mediante cambio de base. Al cambiar el comando pick o reordenar las entradas, puedes hacer que el historial de la rama tenga el aspecto que quieras que tenga. Por ejemplo, si la segunda confirmación arregla un pequeño problema que tenía la primera, puedes agruparlas en una única confirmación con el comando fixup:

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Cuando guardas y cierras un archivo, Git realizará la reorganización en función de tus instrucciones, lo que hará que el historial del proyecto tenga este aspecto:

Combinación de una confirmación con una reorganización interactiva

Eliminar commits irrelevantes como estos facilita mucho la comprensión del historial de la rama de funcionalidades. y es algo que, sencillamente, el comando git merge no puede hacer.

La regla de oro de la reorganización

Una vez comprendida la reorganización, lo más importante es saber cuándo no usarla. La regla de oro de git rebase es no usarlo jamás en ramas públicas.

Por ejemplo, piensa qué ocurriría si usaras este comando para fusionar mediante cambio de base la rama main con tu rama feature:

Reorganización de la rama master

La fusión mediante cambio de base mueve todas las confirmaciones de la rama main al extremo de la rama feature. El problema es que esto solo tiene lugar en tu repositorio. Todos los demás desarrolladores siguen trabajando con la rama main original. Dado que la fusión mediante cambio de base genera nuevas confirmaciones, Git pensará que el historial de tu rama main se ha desviado del de todos los demás.

La única forma de sincronizar las dos ramas main es fusionarlas de nuevo, lo que genera una confirmación de fusión adicional, además de dos conjuntos de confirmaciones que contienen los mismos cambios (los originales y los de la rama fusionada mediante cambio de base). No hace falta decir que resulta bastante confuso.

Así pues, antes de ejecutar el comando git rebase, pregúntate siempre si hay alguien más trabajando en esa rama. Si la respuesta es afirmativa, quita las manos del teclado y empieza a pensar en una forma no destructiva de realizar los cambios (por ejemplo, el comando git revert). En caso contrario, puedes reescribir el historial tanto como quieras.

Envío forzado

Si intentas enviar la rama main fusionada mediante cambio de base de nuevo a un repositorio remoto, Git no te permitirá hacerlo porque entraría en conflicto con la rama main remota. Sin embargo, puedes forzar el envío mediante la marca --flag de esta manera:

# ¡Ten mucho cuidado con este comando! git push --force

Con este comando se sobrescribe la rama main remota para que concuerde con la rama fusionada mediante cambio de base de tu repositorio, de modo que crea confusión al resto de tu equipo. Así pues, ten mucho cuidado y úsalo solo cuando sepas exactamente lo que estás haciendo.

Uno de los pocos casos en los que deberías usarlo es cuando realizas una limpieza local después de enviar una rama de funcionalidades privada a un repositorio remoto (por ejemplo, para realizar una copia de seguridad). Es como decir: "¡Vaya! En realidad no quería enviar esa versión original de la rama de funcionalidades. Toma la actual en su lugar". De nuevo, es importante que nadie esté trabajando a partir de los commits de la versión original de la rama de funcionalidades.

Guía del flujo de trabajo

La reorganización puede incorporarse en tu flujo de trabajo de Git tanto o tan poco como tu equipo quiera. En esta sección, veremos las ventajas que la reorganización puede ofrecer en las diversas etapas del desarrollo de una rama de funcionalidades.

El primer paso de cualquier flujo de trabajo que utiliza el comando git rebase consiste en crear una rama exclusiva para cada rama de función. De este modo, consigues la estructura de rama necesaria para usar la fusión mediante cambio de base con seguridad:

Desarrollo de una rama feature en una rama exclusiva

Limpieza local

Una de las mejores maneras de incorporar la reorganización en tu flujo de trabajo es limpiar las funcionalidades locales en curso. Mediante la realización periódica de una reorganización interactiva, puedes asegurarte de que cada confirmación de tu rama de funcionalidades esté centrada y tenga sentido. Esto te permite escribir el código sin preocuparte de dividirlo en confirmaciones aisladas: puedes corregirlo a posteriori.

Al utilizar el comando git rebase, tienes dos opciones para la nueva base: la rama primaria de tu rama de función (por ejemplo, main) o una confirmación anterior de tu rama de función. Hemos visto un ejemplo de la primera opción en la sección dedicada a la fusión mediante cambio de base interactiva. La segunda opción es ideal cuando solo necesitas arreglar las confirmaciones más recientes. Por ejemplo, el siguiente comando inicia una fusión mediante cambio de base interactiva únicamente de las tres últimas confirmaciones.

git checkout feature git rebase -i HEAD~3

Al especificar HEAD~3 como la nueva base, en realidad no estás moviendo la rama, sino que estas reescribiendo de forma interactiva los 3 commits subsiguientes. Ten en cuenta que los cambios de nivel superior no se incorporarán a la rama feature.

Reorganización en HEAD~3

Si deseas reescribir la rama de funcionalidades por completo usando este método, el comando git merge-base te puede resultar útil para encontrar la base original de tu rama feature. Escribiendo lo siguiente obtendrás el ID del commit de la base original, que luego puedes pasar a git rebase:

 git merge-base feature main

Este uso de la reorganización interactiva es una manera excelente de introducir el comando git rebase a tu workflow, ya que solo afecta a las ramas locales. Lo único que verán los demás desarrolladores es tu producto acabado, que debería ser un historial de rama de funcionalidades limpio y fácil de seguir.

Pero, insistimos, solo funciona con ramas de funcionalidades privadas. Si colaboras con otros desarrolladores por medio de la misma rama de funcionalidades, entonces la rama es pública y no puedes reescribir su historial.

No hay alternativa a git merge para limpiar los commits locales con una reorganización interactiva.

Incorporación de cambios de nivel superior en una rama de funcionalidades

En la sección Resumen de conceptos, hemos visto cómo se pueden incorporar cambios presentes en el repositorio remoto en una rama de función desde una rama main usando git merge o git rebase. La fusión es una opción segura que conserva todo el historial de tu repositorio, mientras que la fusión mediante cambio de base crea un historial lineal al mover la rama de función al extremo de la rama main.

Este uso de git rebase es similar a una limpieza local (y puede realizarse simultáneamente), pero en el proceso incorpora las confirmaciones ascendentes de la rama main.

Recuerda que es perfectamente legítimo hacer una fusión mediante cambio de base de una rama remota en lugar de en la main. Esta situación puede darse cuando colaboras en la misma función con otro desarrollador y necesitas incorporar sus cambios en tu repositorio.

Por ejemplo, si tú y otro desarrollador llamado John añadisteis confirmaciones a la rama feature, tu repositorio podría parecerse a lo siguiente después de recuperar la rama feature remota del repositorio de John:

Colaboración en la misma rama feature

Puedes resolver esta bifurcación exactamente del mismo modo en que integras los cambios de nivel superior de la rama main: puedes fusionar tu rama feature local con john/feature o fusionar mediante cambio de base tu rama feature local en el extremo de john/feature.

Fusión frente a reorganización en una rama remota

Ten en cuenta que esta fusión mediante cambio de base no infringe la regla de oro de la fusión mediante cambio de base, ya que solo se mueven las confirmaciones de tu rama feature local, todo lo anterior permanece intacto. Es como decir "añade mis cambios a lo que John ya ha hecho". En la mayoría de los casos, esto es más intuitivo que la sincronización con la rama remota mediante una confirmación de fusión.

De manera predeterminada, el comando git pull realiza una fusión, pero puedes forzarlo para que integre la rama remota con una fusión mediante cambio de base pasándole la opción --rebase.

Revisión de una rama de funcionalidades con una solicitud de incorporación de cambios

Si utilizas pull requests como parte del proceso de revisión del código, debes evitar usar el comando git rebase después de crear la pull request. En el mismo momento que realices la pull request, otros desarrolladores podrán ver tus commits, lo que significa que la rama pasará a ser pública. Reescribir su historial hará que resulte imposible para Git y para tus compañeros de equipo realizar el seguimiento de cualquier commit añadido a la funcionalidad.

Los cambios hechos por otros desarrolladores deben incorporarse con el comando git merge y no con git rebase.

Por este motivo, generalmente es una buena idea limpiar el código con una reorganización interactiva antes de enviar la pull request.

Integración de una rama de funcionalidades aprobada

Una vez que tu equipo haya aprobado una rama de función, tienes la opción de fusionarla mediante cambio de base en el extremo de la rama main antes de usar git merge para integrar la rama de función en la base de código principal.

Se trata de una situación similar a la de incorporar cambios de nivel superior en una rama de función, pero, como no se pueden reescribir las confirmaciones de la rama main, al final tendrás que usar git merge para integrar la función. Sin embargo, al realizar una fusión mediante cambio de base previa a la fusión, garantizas que esta se hará con avance rápido (fast-forward), lo que da como resultado un historial perfectamente lineal. Además, te brinda la oportunidad de combinar cualquier confirmación posterior añadida durante una solicitud de incorporación de cambios.

Integración de una rama feature en una rama master con y sin una reorganización

Si aún no te sientes del todo cómodo con el comando git rebase, siempre puedes realizar la reorganización en una rama temporal. De este modo, si accidentalmente echas por tierra el historial de tu rama de funcionalidades, puedes extraer la rama original y volver a intentarlo. Por ejemplo:

git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch

Resumen

Y esto es todo lo que debes saber para empezar a reorganizar tus ramas. Si prefieres un historial limpio, lineal y libre de commits de fusión innecesarios, debes usar el comando git rebase en lugar de git merge para integrar los cambios de otra rama.

Por otro lado, si quieres conservar el historial completo de tu proyecto y evitar el riesgo de reescribir las confirmaciones públicas, puedes utilizar git merge. Cualquiera de las dos opciones es perfectamente válida, pero al menos ahora tienes la opción de aprovechar las ventajas de git rebase.