En el sistema de control de versiones Git, existen dos formas de combinar una rama con otra, representadas por diferentes comandos:
git merge. Los commits de una rama se transfieren a otra creando un commit de fusión (merge commit).
git rebase. Los commits de una rama se transfieren a otra mientras se conserva el orden original de los cambios.
En pocas palabras: con git merge, los commits de una rama se “comprimen” en uno solo, mientras que con git rebase permanecen intactos, pero las ramas se combinan igualmente.
El comando git rebase permite, por tanto, combinar los commits de ambas ramas formando un historial compartido de cambios.
Esta guía aborda el comando git rebase, que se encarga de reasignar (rebasar) los commits (cambios) de una rama a otra.
Todos los ejemplos mostrados usan Git versión 2.34.1, ejecutándose en un servidor de Hostman con el sistema operativo Ubuntu 22.04.
Puedes usar estas guías para instalar Git en tu equipo:
Git Rebase es un comando potente que se usa principalmente para integrar cambios de una rama sobre otra reescribiendo el historial de commits. A diferencia de git merge, que crea un nuevo commit de fusión y preserva el historial de ambas ramas, git rebase mueve o “reproduce” una serie de commits de una rama sobre otra. Este proceso genera un historial lineal, que parece como si la rama de funciones se hubiera desarrollado directamente desde el último commit de la rama principal (por ejemplo, main o master). Así, se limpia el historial y se eliminan commits de fusión innecesarios, resultando en una estructura de proyecto más clara y organizada.
La mejor manera de entender cómo funciona rebase en Git es observar un repositorio abstracto compuesto por varias ramas. El proceso debe analizarse paso a paso.
Supongamos que creamos un repositorio con una sola rama, master, que contiene un único commit. La rama master se ve así:
master
commit_1
Luego, a partir de master, creamos una nueva rama llamada hypothesis, donde decidimos probar algunas funciones. En esta nueva rama realizamos varios commits mejorando el código. La rama ahora se ve así:
hypothesis
commit_4
commit_3
commit_2
commit_1
Más tarde, añadimos otro commit a la rama master para corregir urgentemente una vulnerabilidad. La rama master ahora se ve así:
master
commit_5
commit_1
Ahora nuestro repositorio tiene dos ramas:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_1
La rama master es la principal, mientras que hypothesis es secundaria (derivada). Los commits más recientes se muestran encima de los anteriores, como en la salida del comando git log.
Supongamos que queremos seguir trabajando en la función que habíamos movido a la rama hypothesis. Sin embargo, esta rama no contiene la corrección crítica que hicimos en master.
Por lo tanto, queremos “sincronizar” el estado de hypothesis con master para que el commit de corrección también aparezca en la rama de funciones. En otras palabras, queremos que la estructura del repositorio se vea así:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Como puedes ver, ahora hypothesis repite exactamente el historial de master, aunque originalmente se creó antes de commit_5. Es decir, hypothesis ahora contiene el historial de ambas ramas: la suya y la de master.
Para lograrlo, debemos usar git rebase.
Después, los cambios hechos en hypothesis pueden fusionarse en master usando el comando clásico git merge, que crea un commit de fusión.
La estructura del repositorio se verá así:
master
commit_merge
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Además, ejecutar git merge después de git rebase puede reducir la probabilidad de conflictos.
Una vez cubierto el aspecto teórico del comando git rebase, podemos probarlo en un repositorio real de un proyecto de ejemplo. La estructura del repositorio será la misma que en el ejemplo teórico anterior.
Primero, crea un directorio separado para el repositorio:
mkdir rebase
Luego entra en él:
cd rebase
Ahora podemos inicializar el repositorio:
git init
En la consola aparecerá un mensaje informativo estándar:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
...
Y en el directorio actual aparecerá una carpeta oculta .git, que puedes ver con:
ls -a
El parámetro -a significa all y permite ver el sistema de archivos en modo extendido. Su contenido será:
. .. .git
Antes de crear commits, debemos configurar algunos datos básicos del usuario.
Primero, el nombre:
git config --global user.name "NAME"
Luego el correo electrónico:
git config --global user.email "NAME@HOST.COM"
Usando archivos de texto simples, simularemos la adición de diferentes funciones al proyecto. Cada nueva función se representará como un commit separado.
Crea un archivo para la primera función:
nano function_1
Rellénalo con el siguiente contenido:
Function 1
Ahora indexa los cambios:
git add .
Verifica el estado de indexación:
git status
En la consola debería aparecer un mensaje indicando los cambios indexados:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: function_1
Ahora podemos hacer commit:
git commit -m "commit_1"
La consola mostrará un mensaje confirmando el commit exitoso en master:
[master (root-commit) 4eb7cc3] commit_1
1 file changed, 1 insertion(+)
create mode 100644 function_1
Ahora crea una nueva rama llamada hypothesis:
git checkout -b hypothesis
El parámetro -b sirve para cambiar inmediatamente a la nueva rama.
La consola mostrará un mensaje de confirmación:
Switched to a new branch 'hypothesis'
A continuación, debemos realizar tres commits consecutivos con tres archivos, igual que en master:
commit_2 con el archivo function_2 y contenido: Function 2commit_3 con el archivo function_3 y contenido: Function 3commit_4 con el archivo function_4 y contenido: Function 4Si luego comprobamos la lista de commits:
git log --oneline
La consola mostrará la siguiente secuencia:
d3efb82 (HEAD -> hypothesis) commit_4
c9f57b7 commit_3
c977f16 commit_2
4eb7cc3 (master) commit_1
Aquí, la opción --oneline se usa para mostrar la información de los commits en formato resumido.
El último paso es añadir otro commit a la rama principal master. Cambia a ella:
git checkout master
La consola confirmará el cambio:
Switched to branch 'master'
Crea otro archivo de función:
nano function_5
Con el siguiente contenido:
Function 5
Indexa los cambios:
git add .
Y haz el nuevo commit:
git commit -m "commit_5"
Si comprobamos la lista de commits:
git log --oneline
La rama master ahora tendrá dos commits:
3df7a00 (HEAD -> master) commit_5
4eb7cc3 commit_1
Para realizar el rebase, primero hay que cambiar a la rama hypothesis:
git checkout hypothesis
Y ejecutar el rebase:
git rebase master
Después, la consola mostrará un mensaje confirmando un rebase exitoso:
Successfully rebased and updated refs/heads/hypothesis.
Ahora puedes comprobar la lista de commits:
git log --oneline
La consola mostrará una lista con los commits de ambas ramas en el orden original:
8ecfd58 (HEAD -> hypothesis) commit_4
f715aba commit_3
ee47470 commit_2
3df7a00 (master) commit_5
4eb7cc3 commit_1
Ahora la rama hypothesis contiene el historial completo del repositorio.
Al igual que con git merge, al usar el comando git rebase pueden ocurrir conflictos que requieren resolución manual.
Crea otro archivo en la rama hypothesis:
nano conflict
Y escribe el siguiente texto:
There must be a conflict here!
Indexa los cambios:
git add .
Haz un commit:
git commit -m "conflict_1"
Ahora cambia a la rama master:
git checkout master
Crea un archivo similar:
nano conflict
Y agrega el siguiente contenido:
There must NOT be a conflict here!
Indexa los cambios:
git add .
Haz un commit:
git commit -m "conflict_2"
Modifica nuevamente el archivo:
nano conflict
Cámbialo por:
There definitely must NOT be a conflict here!
Indexa y haz otro commit:
git add .
git commit -m "conflict_3"
Cambia de nuevo a hypothesis y ejecuta:
git checkout hypothesis
git rebase master
La consola mostrará un mensaje de conflicto:
Auto-merging conflict
CONFLICT (add/add): Merge conflict in conflict
error: could not apply 6003ed7... conflict_1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6003ed7... conflict_1
Git sugiere editar el archivo de conflicto, indexar los cambios con git add y luego continuar con --continue.
nano conflict
El archivo contendrá dos versiones en conflicto dentro de marcadores especiales:
<<<<<<< HEAD
There definitely must NOT be a conflict here!
=======
There must be a conflict here!
>>>>>>> 6003ed7 (conflict_1)
Nuestro trabajo es eliminar las partes innecesarias y dejar una versión final:
There must absolutely definitely unanimously NOT be any conflict here!
Indexa los cambios y continúa:
git add .
git rebase --continue
Después de esto, la consola abrirá un editor de texto que te sugerirá modificar el mensaje de confirmación original de la confirmación donde ocurrió el conflicto:
conflict_1
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd7aefc
# Last commands done (4 commands done):
# pick 8ecfd58 commit_4
# pick 6003ed7 conflict_1
# No commands remaining.
# You are currently rebasing branch 'hypothesis' on 'bd7aefc'.
#
# Changes to be committed:
# modified: conflict
#
La consola mostrará un mensaje indicando que el proceso de rebase se ha completado correctamente:
[detached HEAD 482db49] conflict_1
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/hypothesis.
Ahora, si revisas la lista de confirmaciones en la rama de hipótesis:
git log --oneline
Verás la secuencia original de todos los cambios realizados:
482db49 (HEAD -> hypothesis) conflict_1
bd5d036 commit_4
407e245 commit_3
948b41c commit_2
bd7aefc (master) conflict_3
d98648d conflict_2
3df7a00 commit_5
4eb7cc3 commit_1
Observa que las confirmaciones conflict_2 y conflict_3, realizadas en la rama master, aparecen en el historial antes que la confirmación conflict_1. Sin embargo, esto aplica a todas las confirmaciones realizadas en la rama master.
Además de trabajar con ramas locales, también es posible hacer rebase al extraer cambios de un repositorio remoto. Para ello, se añade la opción --rebase al comando pull:
git pull --rebase remote branch
Donde:
remote es el repositorio remoto
branch es la rama remota
Básicamente, esta configuración de pull es equivalente a git rebase, salvo que los commits aplicados en la rama actual provienen del repositorio remoto.
El comando git rebase permite crear un historial lineal en la rama de destino, compuesto por commits secuenciales. Esta secuencia sin ramificaciones hace que el historial sea más fácil de leer y entender.
Ejecutar git rebase antes puede reducir significativamente la probabilidad de conflictos al fusionar ramas con git merge. Los conflictos son más fáciles de resolver en commits secuenciales que en uno solo de fusión. Esto es especialmente útil al enviar ramas a repositorios remotos.
A diferencia de merge, rebase reescribe parcialmente el historial de la rama de destino, eliminando elementos innecesarios.
La capacidad de reestructurar significativamente el historial puede causar errores irreversibles en el repositorio, lo que podría provocar la pérdida permanente de datos.
Git rebase es especialmente útil al trabajar en ramas pequeñas o de características individuales que finalmente se fusionarán en la rama principal. También es ideal para mantener un historial limpio y lineal, lo que resulta beneficioso en proyectos de código abierto o cuando es necesario un historial claro para depuración y comprensión.
Sin embargo, en entornos colaborativos con varios desarrolladores, se debe usar con precaución para evitar problemas por reescritura de historial público. Es importante comunicar al equipo cuándo es apropiado rebasar. En muchos casos, git merge puede ser más seguro y simple.
Uno de los aspectos críticos de git rebase es que reescribe el historial de commits, lo que cambia los identificadores SHA-1. Esto puede generar problemas en entornos colaborativos, especialmente si se rebasea una rama que ya fue enviada a un remoto. Esto puede provocar conflictos al intentar hacer push.
Como práctica recomendada, evita hacer rebase a ramas públicas que ya han sido compartidas, ya que esto puede crear historiales divergentes y conflictos difíciles de resolver. Git rebase es más adecuado para ramas locales o para preparar una rama de funciones antes de fusionarla con la principal.
Durante un rebase, si existen cambios conflictivos entre ramas, Git se detendrá y pedirá resolverlos manualmente. Aunque la resolución es similar a la de merge, puede requerir más trabajo, especialmente en ramas largas. Después de resolver los conflictos, puedes continuar con:
git rebase --continue
Combinar dos ramas usando rebase, mediante el comando git rebase, es fundamentalmente diferente del merge clásico con git merge.
git merge combina los commits de una rama en un solo commit dentro de otra.
git rebase mueve los commits de una rama al final de otra, manteniendo el orden original.
Un efecto similar se puede lograr usando git pull con la opción --rebase.
Por un lado, git rebase permite mantener un historial más limpio y comprensible en la rama principal, mejorando la mantenibilidad del repositorio.
Por otro lado, git rebase simplifica la historia eliminando algunos detalles, lo que puede reducir la trazabilidad de los cambios.
Por esta razón, rebase es una herramienta pensada para usuarios experimentados que entienden cómo funciona Git. A menudo, git rebase se usa junto con git merge para lograr una estructura óptima del repositorio y las ramas.