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 reorganización 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.

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

A forked commit history

Digamos ahora que las nuevas confirmaciones de la rama master son necesarias 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 reorganización (rebase).

La opción de fusión

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

git checkout feature
git merge master

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

git merge feature master

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:

Merging master into the feature branch

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 master es muy activa, esto puede contaminar bastante el historial de tu rama de función. Mientras que es posible mitigar este problema con opciones avanzadas de git log, puede hacer más difícil que otros desarrolladores entiendan el historial del proyecto.

La opción de reorganización

Como alternativa a la fusión, puedes reorganizar la rama feature en la rama master mediante los siguientes comandos:

git checkout feature
git rebase master

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

Rebasing the feature branch onto master

La principal ventaja es que el historial del proyecto queda mucho más limpio. En primer lugar, se eliminan las confirmaciones de fusión innecesarias 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 confirmaciones 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 flujo de trabajo de colaboración. Y, aunque menos importante, también se pierde el contexto que proporciona la confirmación de fusión: no se puede ver cuándo se han incorporado los cambios de nivel superior a la función.

Reorganización interactiva

La reorganización interactiva te brinda la oportunidad de modificar las confirmaciones al moverlas a la nueva rama. Es aún más potente que una reorganización automatizada porque te otorga 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 master.

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

git checkout feature
git rebase -i master

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 reorganización. 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 confirmación, 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:

Squashing a commit with an interactive rebase

Eliminar confirmaciones irrelevantes como estas facilita mucho la comprensión del historial de la rama de funciones 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 en lo que pasaría si reorganizaras tu rama master en tu rama feature:

Rebasing the master branch

La reorganización mueve todas las confirmaciones de la rama master 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 master original. Dado que la reorganización genera nuevas confirmaciones, Git pensará que el historial de tu rama master se ha desviado del de todos los demás.

La única forma de sincronizar las dos ramas master 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 reorganizada). 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 en esa rama. Si la respuesta es afirmativa, levanta 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 master reorganizada de nuevo a un repositorio remoto, Git no te permitirá hacerlo porque entraría en conflicto con la rama master remota. Sin embargo, puedes forzar el envío mediante la marca --force, así:

# Be very careful with this command!
git push --force

Con este comando se sobrescribe la rama master remota para que concuerde con la rama reorganizada 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 las confirmaciones 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 funcionalidades. De este modo, consigues la estructura de rama necesaria para usar la reorganización con seguridad:

Developing a feature in a dedicated branch

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 funcionalidades (por ejemplo, la master) o una confirmación anterior de tu rama de funcionalidades. Hemos visto un ejemplo de la primera opción en la sección dedicada a la reorganización interactiva. La segunda opción es ideal cuando solo necesitas arreglar las confirmaciones más recientes. Por ejemplo, el siguiente comando inicia una reorganización interactiva únicamente de las últimas tres 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 las 3 confirmaciones subsiguientes. Ten en cuenta que los cambios de nivel superior no se incorporarán a la rama feature.

Rebasing onto 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 de la confirmación de la base original, que luego puedes pasar a git rebase:

git merge-base feature master

Este uso de la reorganización interactiva es una manera excelente de introducir el comando git rebase en tu flujo de trabajo, 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 las confirmaciones 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 de nivel superior en una rama de funcionalidades desde una rama master 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 reorganización crea un historial lineal al mover la rama de funcionalidades al extremo de la rama master.

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

Recuerda que es perfectamente legítimo hacer una reorganización de una rama remota en lugar de la master. Esta situación puede darse cuando colaboras en la misma funcionalidad con otro desarrollador y necesitas incorporar sus cambios en tu repositorio.

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

Collaborating on the same feature branch

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

Merging vs. rebasing onto a remote branch

Ten en cuenta que esta reorganización no infringe la regla de oro de la reorganización, ya que solo se mueven las confirmaciones de tu rama feature local, todo lo anterior permanece intacto. Es como decir “añade mis cambios en 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 reorganización pasándole la opción --rebase.

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

Si utilizas solicitudes de incorporación de cambios como parte del proceso de revisión del código, debes evitar usar el comando git rebase después de crear la solicitud de incorporación de cambios. En el mismo momento que realices la solicitud de incorporación de cambios, otros desarrolladores podrán ver tus confirmaciones, 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 confirmación añadida 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 solicitud de incorporación de cambios.

Integración de una rama de funcionalidades aprobada

Una vez que tu equipo ha aprobado una rama de funcionalidades, tienes la opción de reorganizarla en el extremo de la rama master antes de usar git merge para integrar la rama de funcionalidades 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 funcionalidades, pero, como no se pueden reescribir las confirmaciones de la rama master, al final tendrás que usar git merge para integrar la funcionalidad. Sin embargo, al realizar una reorganización previa a la fusión, garantizas que esta se haga con avance rápido, lo que da como resultado un historial perfectamente lineal. Además, te brinda la oportunidad de combinar cualquier confirmación de seguimiento añadida durante una solicitud de incorporación de cambios.

Integrating a feature into master with and without a rebase

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 master
# [Clean up the history]
git checkout master
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 confirmaciones de fusión innecesarias, 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.