Skip to main content

Git, de forma sencilla: cómo cambiar el historial con rebase—parte I

Escrito por
Headshot of Or Kamara

Or Kamara

9 de junio de 2020

0 minutos de lectura

Últimamente, estaba trabajando en una funcionalidad grande que incluía varios cambios en distintas áreas del código. Para que el PR de esta funcionalidad fuera fácil de revisar, dividí toda la funcionalidad en varios commits. Con el tiempo, me di cuenta rápidamente de que esas divisiones no eran suficientes: tenía que hacer muchos cambios en cada uno de esos commits (por algunos problemas que encontré o correcciones de CR).

En ese momento, ya conocía los fundamentos del mágico comando interactivo git rebase y sabía que podías usarlo para «reescribir tu historial». Pero los cambios que tenía que hacer eran más complicados de lo que sabía, así que empecé a explorar las opciones de este comando.

Para quienes no conocen el comando, este es el resultado de ejecutar git rebase -i BRANCH:

pick 271c66e feat: 1st commit
pick 51588eb feat: 2nd commit
pick 4b339f2 feat: 3rd commit

# Rebase 24aa86a..4b339f2 onto 24aa86a (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
...

Recuerdo perfectamente la primera vez que ejecuté este comando. Mi reacción fue algo así: Bueno, ¿qué está pasando aquí? Alguien me había prometido una forma interactiva y sencilla de cambiar mis commits, pero lo único que veía era una interfaz muy complicada… Literalmente tuve que dedicar tiempo a leer sobre cada una de esas opciones, porque al salir de esta pantalla podía perder todo el trabajo que había hecho (#DesarrolladorAsustado).

Incluso cuando intenté salir de la pantalla y cancelar todo, no fue una experiencia sencilla… Tuve que buscar en Google cómo hacerlo y descubrí que tenía que eliminar todas las líneas de commits para que no hubiera cambios. Así de fácil (y poco elegante). Lo primero que pensé fue: «Pero espera, no confío del todo en todo lo que dice Google. ¿Y si lo hago y desaparece todo mi trabajo?»

Entonces decidí que no había otra opción y empecé a investigar esos comandos.

En este artículo, quiero explicar distintos problemas que los desarrolladores deben resolver al trabajar en PR con varios commits. No explicaré cada detalle de esos comandos, ya que hay varios artículos que explican qué sucede tras bambalinas. En cambio, te presentaré algunas situaciones que cualquiera de nosotros podría encontrar al trabajar con git y sugeriré la solución adecuada con git rebase -i.

Como este tema es bastante extenso, dividiré esta publicación en dos partes. En esta primera parte repasaré los fundamentos de Rebase y dos situaciones comunes. En la segunda parte, presentaré otras cuatro situaciones comunes y algunas conclusiones.

¡Sin más preámbulos, empecemos con la primera parte!

Rebase en pocas palabras

Empecemos por la siguiente situación: tú y un amigo trabajan en un nuevo proyecto emocionante. Acabas de crear el repositorio (y hacer un primer commit llamado A) y ahora es hora de programar. Para trabajar de forma más eficiente, decidieron dividirse las tareas. Tú te encargas de la funcionalidad principal y tu amigo de crear la estructura de la aplicación.

Así que ambos parten del mismo punto: el commit A. Después de 5 horas de trabajo intenso, quieres terminar y combinar los cambios con master. Tu amigo es el primero en combinar sus cambios y crea 3 commits nuevos sobre A: B + C + D. Ahora es tu turno de combinar con master todo tu trabajo de la FeatureBranch: los commits E + F + G. Así se ve:

      E----F----G FeatureBranch
     /
    A----B----C----D master

Para probar la funcionalidad que creaste, necesitas usar algunas funciones del framework, más específicamente, las del commit D. En otras palabras, necesitas cambiar la base de tu rama del commit A al commit D, como si hubieras creado tu rama desde la versión más reciente:

                     E----F----G FeatureBranch
                    /
    A----B----C----D master

Este flujo explica exactamente por qué se necesita git rebase: para mover una secuencia de commits a un nuevo commit base. Tras bambalinas, git crea commits nuevos y los aplica a la nueva base especificada.

Más específicamente, en este artículo hablaremos del modo interactivo.

Al ejecutar rebase con la opción -i, puedes modificar commits individuales como parte del proceso básico de rebase. ¡Aquí es donde empieza la magia! En el modo interactivo, puedes elegir qué acción quieres realizar en cada uno de tus commits: combinar, dividir, cambiar el nombre, reordenar y más.

Intentaremos entender cada una de estas opciones con situaciones comunes en las que git rebase puede ser útil.

Situaciones comunes (¡y cómo resolverlas!)

Veamos algunas situaciones comunes en las que podrías querer cambiar el historial de tu código y cómo hacerlo con Git:

1. Cambiar el mensaje de un commit anterior (reword)

Hicimos varios cambios en el código y los dividimos perfectamente en distintos commits, pero luego notamos que hay un error tipográfico en uno de los mensajes de commit:

* 3d320d9 (HEAD -> feat/git-history) feat: 3rd commit
* 03d03e9 feat: 3nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

Como probablemente ves, el mensaje del segundo commit es 3nd commit (ups); vamos a corregirlo.

Si el error tipográfico estuviera en el último commit, podríamos haber usado el comando git commit --amend. Pero como está en medio del grafo de git, necesitamos hacer algo más complicado.

En este caso, usaremos el comando git rebase -i master (master puede ser otra rama) y elegiremos la opción reword para el segundo commit (a21bd1d):

pick 271c66e feat: 1st commit
reword 03d03e9 feat: 3nd commit
pick 3d320d9 feat: 3rd commit

Después de guardar el archivo, se abrirá un nuevo editor con la opción de editar el mensaje del commit:

feat: 3nd commit

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun May 17 16:28:13 2020 +0300
#
# interactive rebase in progress; onto 24aa86a
# Last commands done (2 commands done):
#    pick 271c66e feat: 1st commit
#    r 03d03e9 feat: 3nd commit
# Next command to do (1 remaining command):
#    pick 3d320d9 feat: 3rd commit
# You are currently editing a commit while rebasing branch 'feat/git-history' on '24aa86a'.
#
# Changes to be committed:
#       modified:   b.txt
#

Lo siguiente es cambiar la primera línea por el nuevo mensaje de commit que queremos, feat: 2nd commit, y volver a guardar.

El grafo de git ya está corregido:

* fdb5f71 (HEAD -> feat/git-history) feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

2. Cambiar el contenido de un commit anterior (squash, fixup)

Acabas de recibir comentarios de CR de tu compañero y tienes que hacer algunos cambios pequeños: agregar un archivo nuevo y cambiar el contenido de uno existente.

Como probablemente sabes, cada commit debería representar un bloque de lógica, no solo cambios en archivos. Una de las cosas que debemos evitar es tener mensajes de commit como CR fixes o, en nuestro caso, incluso really small CR fixes. Lo ideal es combinar todos los commits para que, al final, cada commit incluya todos los cambios pertinentes (incluidas las correcciones de CR).

¿Cómo lo hacemos? Para eso sirven las opciones squash y fixup.

Primero, debemos crear commits temporales con los cambios necesarios:

* 17a73c1 (HEAD -> feat/git-history) CR fix: add new file to 2nd commit
* 5641a86 CR fix: content of file in 1st commit
* fdb5f71 feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

En el ejemplo anterior, creamos 2 commits nuevos:

  • 5641a86: cambia el contenido del archivo que creamos en el primer commit

  • 17a73c1: agrega un archivo nuevo al segundo commit

Pero ahora queremos evitar que el grafo de git quede así, y combinar 5641a86 con 271c66e y 17a73c1 con e2952e7. Para eso, debemos ejecutar el comando git rebase -i master. Como la vez anterior, esta es la primera pantalla que veremos:

pick 271c66e feat: 1st commit
pick e2952e7 feat: 2nd commit
pick fdb5f71 feat: 3rd commit
pick 5641a86 cr fix: content of file in 1st commit
pick 17a73c1 cr fix: add new file to 2nd commit

Ahora, cambiemos el orden de las líneas según el orden deseado de los commits y usemos la opción squash en los commits 5641a86 y 17a73c1:

pick 271c66e feat: 1st commit
squash 5641a86 cr fix: content of file in 1st commit
pick e2952e7 feat: 2nd commit
squash 17a73c1 cr fix: add new file to 2nd commit
pick fdb5f71 feat: 3rd commit

Como resultado de estos cambios, git empezará a procesar los commits (desde 271c66e hasta fdb5f71) y combinará el contenido de los commits marcados con squash con el commit anterior. ¡El grafo de git ahora se ve exactamente como queríamos!

* 1b2f5b1 (HEAD -> feat/git-history) feat: 3rd commit
* 6871683 feat: 2nd commit
* 682c27f feat: 1st commit
* 24aa86a (master) Init: Git History

Otra opción que podemos usar en lugar de squash es fixup. Ambas hacen prácticamente lo mismo, excepto que fixup descarta el mensaje del commit.

Una nota importante: a veces, al combinar commits, podemos provocar conflictos en el código. Cuando git detecta conflictos, te pedirá que los resuelvas, prepares los cambios y continúes el proceso de rebase con git rebase --continue. Recuerda que siempre puedes ejecutar git rebase --abort si no tienes claro qué hacer. Este comando detiene el proceso de rebase y devuelve el repositorio a su estado original (antes de ejecutar git rebase -i).

Para terminar

¡Eso es todo por la primera parte sobre cómo cambiar el historial de git con Rebase! En la segunda parte, veremos otras cuatro situaciones comunes y sugeriremos cómo resolverlas.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Publicado en: