Manteniendo la historia limpia usando git rebase

Un beneficio que tiene interactuar día a día con otros desarrolladores es que podemos potenciar entre nosotros las buenas prácticas y mejorar en nuestra forma de programar. En Platanus tenemos herramientas que nos permiten mantener el código limpio y ordenado.

Pero los estándares no bastan, para asegurar la calidad tenemos un proceso de revisión de código. Como utilizamos GitHub, este proceso lo realizamos por medio de pull requests y es muy típico que, a pesar de usar linters, hayan detalles que se nos pasen al escribir código.

En esta dinámica de revisión, los comentarios y solicitudes de cambios abundan, muchas veces se trata de un refactor, sacar un typo, o mejoras menores, pero a la hora de programar esos cambios, una duda muy natural surge:

¿cómo deberían verse estos cambios en los commits del pull request?

El problema radica en que muchas veces los commits para arreglar esos cambios se ven como:

El verdadero cambio está en el primer commit, el segundo no aporta mucho

Pero no es deseable incluir ese tipo de commits en la historia de un repositorio, porque no tienen que ver realmente con lo que se cambió en la lógica del proyecto. Lo único que se logra es contaminar la historia y hacer menos útil ese historial.

No solo ensucian la historia, también dificultan la revisión, ya que cuando se revisa un pull request commit a commit, quien revisa puede encontrar un typo en un commit anterior, y después se da cuenta que subieron otro commit que lo soluciona. Todo un enredo que hace que la revisión sea menos fructífera.


Aquí es donde git nos hace la vida más fácil con rebase. Esta herramienta tiene múltiples usos, pero uno que encuentro particularmente útil es el de editar la historia de commits en una branch, y explicaré una forma de usarlo.

Existe el comando git rebase --interactive '<commit_hash>^' que nos permite reescribir la historia en la rama actual desde el commit al que corresponde ese hash, hasta el último. Al ejecutarlo se abre en la consola un documento con la planificación para este rebase, que corresponde a una lista de los commits correspondientes.

Así se ve el archivo de texto que se abre en el terminal con la planificación del rebase

A la izquierda de cada commit aparece una palabra que es la acción a realizar. Por defecto aparece pick que corresponde a seleccionar el commit sin cambiarlo. Si uno quiere cambiar la historia puede cambiar esa palabra por una serie de opciones: edit para modificarlo, reword para cambiar el mensaje, drop para descartar ese commit, squash para juntar ese commit con otro, entre otras.

Me voy a detener en edit que es el que más uso. Una vez que uno cambió en el documento la palabra pick por edit en los commits que quiere modificar, puede guardar y cerrar.

Comúnmente el editor de texto que aparece es vim, a menos que la variable de entorno EDITOR del terminal señale otro, por lo que para editar el texto se presiona la tecla i seguido de lo que se quiere escribir, y para terminar se puede apretar la tecla esc para luego escribir :wq o :x.

Cambié los dos primeros pick, uno por edit y el otro por reword

Cuando se cierra la planificación se ejecuta realmente el comando, y empieza la magia. Una forma fácil de imaginarse lo que hace el comando es que está viajando al pasado. Git se encarga de ejecutar la acción especificada en cada commit, partiendo desde el más viejo. Cuando la acción es pick no hace nada, porque el commit se mantiene, pero cuando la acción es edit, se detiene en ese commit, como si los commits posteriores aún no ocurrieran.

Algo así está pasando

En ese momento es cuando uno puede editar lo que está pasando. El commit ya está hecho, pero git nos permite modificarlo in situ. Una forma es la siguiente:

  1. Editar y guardar los archivos que queremos cambiar
  2. Hacer git add de los cambios que queremos agregar
  3. Añadir esos cambios al commit que estamos editando, por medio de git commit —-amend —-no-edit

Hay que considerar que realmente el commit ya se hizo, lo que se está haciendo es pararse justo después de que se realizó, y hacer un amend a este commit, es decir, añadirle más cambios, pero sin cambiar nada más, ni el mensaje.

Una vez que ya se está satisfecho con los cambios realizados, hay que indicar que puede continuar, y eso se hace por medio de git rebase —-continue. Así el comando sigue realizando las acciones que correspondan sobre los commits restantes, hasta llegar al último.

Aquí se ve el ejemplo completo. Tip: En el log se puede obtener el hash de cada commit.

Otra de las acciones que mencioné antes es reword, que en vez de modificar el contenido de cada commit, permite modificar el mensaje. La acción squash en términos simples se puede usar para mezclar uno o varios commits que no se quieren mantener, en el anterior. Es una forma alternativa de mantener la historia limpia.

Ahora solo queda hacer push de la rama con los cambios, pero al intentar hacerlo es probable que sea rechazado:

Esto pasa porque la historia no coincide con lo que se había subido anteriormente en esa rama.

No hay que preocuparse, se puede forzar con git push —-force o el más seguro git push —-force-with-lease origin <branch_name>, que evita pisar el trabajo ajeno y nos asegura subir cambios en la rama correcta. Disclaimer: no suelo recomendar utilizar comandos con flags como esos, pero como estamos en una rama distinta a la principal y los cambios son solicitados en el pull request, no debería haber problema. Igual tengo que recomendar usar el comando con cuidado y en la rama que corresponde, nunca en master.


En lo personal me gustan los repositorios ordenados y poder navegar por los commits. La gran gracia de usar rebase de la forma que vimos es que la historia del repositorio queda mucho más limpia que agregando un commit por cada mini cambio que nos pidan en la revisión. Para los obsesivos como yo, no solo es útil, es necesario.