Programa
"Commit early, commit often" es un mantra popular en el desarrollo de software cuando se utiliza Git. Hacerlo garantiza que cada cambio esté bien documentado, mejora la colaboración y facilita el seguimiento de la evolución del proyecto. Sin embargo, esto también puede llevar a una sobreabundancia de commits.
Aquí es donde entra en juego la importancia de aplastar los commits. Aplastar confirmaciones es el proceso de combinar varias entradas de confirmación en una única confirmación cohesionada.
Conviértete en Ingeniero de Datos
Por ejemplo, supongamos que trabajamos en una función que implementa un formulario de inicio de sesión, y creamos los cuatro commits siguientes:
Una vez completada la función, para el proyecto en general, estos commits son demasiado detallados. No necesitamos saber en el futuro que nos encontramos con un error que se solucionó durante el desarrollo. Para garantizar un historial limpio en la rama principal, aplastamos estos commits en un único commit:
Cómo aplastar commits en Git: Rebase interactivo
El método más común para aplastar confirmaciones es utilizar un rebase interactivo. Lo iniciamos utilizando el comando
git rebase -i HEAD~<number_of_commits>
Sustituye por el número de commits que queremos aplastar.
En nuestro caso, tenemos cuatro commits, por lo que el comando es:
git rebase -i HEAD~4
Al ejecutar este comando se abrirá un editor interactivo de la línea de comandos:
La sección superior muestra los commits, mientras que la inferior contiene comentarios sobre cómo aplastar los commits.
Vemos cuatro compromisos. Para cada una, debemos decidir qué orden ejecutar. Nos interesan los comandos pick
(p
) y squash
(s
). Para aplastar estas cuatro confirmaciones en una sola, podemos elegir la primera y aplastar las tres restantes.
Aplicamos los comandos modificando el texto que precede a cada confirmación, concretamente cambiando pick
por s
o squash
para la segunda, tercera y cuarta confirmaciones. Para hacer estas modificaciones, tenemos que entrar en el modo "INSERTAR" en el editor de texto de la línea de comandos pulsando la tecla i
del teclado:
Tras pulsar i
, aparecerá el texto -- INSERT --
en la parte inferior, indicando que hemos entrado en el modo de inserción. Ahora podemos mover el cursor con las teclas de flecha, borrar caracteres y escribir como lo haríamos en un editor de texto estándar:
Cuando estemos satisfechos con los cambios, debemos salir del modo de inserción pulsando la tecla Esc
del teclado. El siguiente paso es guardar nuestros cambios y salir del editor. Para ello, primero pulsamos la tecla :
para indicar al editor que pretendemos ejecutar un comando:
En la parte inferior del editor, ahora vemos un punto y coma :
que nos pide que insertemos un comando. Para guardar los cambios, utilizamos el comando w
, que significa "escribir". Para cerrar el editor, utiliza q
, que significa "salir". Estos comandos pueden combinarse y escribirse juntos wq
:
Para ejecutar la orden, pulsamos la tecla Enter
. Esta acción cerrará el editor actual y abrirá uno nuevo, permitiéndonos introducir el mensaje de confirmación para la nueva confirmación aplastada. El editor mostrará un mensaje por defecto con los mensajes de los cuatro commits que estamos aplastando:
Recomiendo modificar el mensaje para que refleje con exactitud los cambios implementados por estos commits combinados; al fin y al cabo, el objetivo del squashing es mantener un historial limpio y fácil de leer.
Para interactuar con el editor y editar el mensaje, volvemos a pulsar i
para entrar en modo edición y editar el mensaje a nuestro gusto.
En este caso, sustituimos el mensaje de confirmación por "Implementar formulario de inicio de sesión". Para salir del modo edición, pulsamos Esc
. A continuación, guarda los cambios pulsando :
, introduciendo el comando wq
y pulsando Enter
.
Cómo ver el historial de confirmaciones
En general, recordar todo el historial de confirmaciones puede ser un reto. Para ver el historial de confirmaciones, podemos utilizar el comando git log
. En el ejemplo mencionado, antes de realizar el squash, al ejecutar el comando git log
se mostraría:
Para navegar por la lista de commits, utiliza las teclas de flecha arriba y abajo. Para salir, pulsa q
.
Podemos utilizar git log
para confirmar el éxito de la calabaza. Ejecutarlo después del squash mostrará una única confirmación con el nuevo mensaje:
Empujar el commit aplastado
El comando anterior actuará sobre el repositorio local. Para actualizar el repositorio remoto, tenemos que empujar nuestros cambios. Sin embargo, como hemos cambiado el historial de confirmaciones, tenemos que forzar el push utilizando la opción --force
:
git push --force origin feature/login-form
Forzar el empuje sobrescribirá el historial de confirmaciones de la rama remota y podría perturbar a otros que trabajen en esa rama. Es una buena práctica comunicarse con el equipo antes de hacerlo
Una forma más segura de forzar el push, que reduce el riesgo de interrumpir a los colaboradores, es utilizar en su lugar la opción --force-with-lease
:
git push --force-with-lease origin feature/login-form
Esta opción garantiza que sólo forzaremos el push si la rama remota no se ha actualizado desde nuestro último fetch o pull.
Aplastar confirmaciones específicas
Imagínate que tenemos 5 compromisarios:
Supongamos que queremos conservar los commits 1, 2 y 5 y eliminar los commits 3 y 4.
Al utilizar el rebase interactivo, las confirmaciones marcadas para ser aplastadas se combinarán con la confirmación directamente anterior. En este caso, significa que queremos aplastar Commit4
para que se fusione con Commit3
.
Para ello, debemos iniciar un rebase interactivo que incluya estos dos commits. En este caso, tres confirmaciones son suficientes, así que utilizamos el comando
git rebase -i HEAD~3
A continuación, ajustamos Commit4
a s
para que quede aplastada con Commit3
:
Tras ejecutar este comando y listar los commits, observamos que los commits 3 y 4 se han aplastado juntos, mientras que el resto permanece sin cambios.
Aplastar a partir de una confirmación específica
En el comando git rebase -i HEAD~3
, la parte HEAD
es una abreviatura de la confirmación más reciente. La sintaxis ~3
se utiliza para especificar un antepasado de una confirmación. Por ejemplo, HEAD~1
se refiere al padre de la confirmación HEAD
.
En un rebase interactivo, las confirmaciones consideradas incluyen todas las confirmaciones antepasadas que conducen a la confirmación especificada en el comando. Ten en cuenta que no se incluye la confirmación especificada:
En lugar de utilizar HEAD, podemos especificar directamente un hash de confirmación. Por ejemplo, Commit2
tiene un hash de dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
, por lo que el comando:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
iniciaría un rebase considerando todos los commits realizados después de Commit2
. Por tanto, si queremos iniciar un rebase en una confirmación concreta e incluir esa confirmación, podemos utilizar el comando
git rebase -i <commit-hash>~1
Resolver conflictos al aplastar commits
Cuando aplastamos confirmaciones, combinamos los cambios de varias confirmaciones en una sola, lo que puede provocar conflictos si los cambios se solapan o divergen significativamente. He aquí algunas situaciones habituales en las que pueden surgir conflictos:
- Cambios superpuestos: Si dos o más confirmaciones que están siendo aplastadas han modificado las mismas líneas de un archivo o líneas estrechamente relacionadas, puede que Git no sea capaz de reconciliar automáticamente estos cambios.
- Diferentes cambios de estado: Si un commit añade un determinado fragmento de código y otro commit modifica o elimina ese mismo fragmento de código, aplastar estos commits puede dar lugar a conflictos que hay que resolver.
- Renombrar y modificar: Si una confirmación cambia el nombre de un archivo y las siguientes hacen cambios en el nombre antiguo, aplastar estas confirmaciones puede confundir a Git, provocando un conflicto.
- Cambios en los archivos binarios: Los archivos binarios no se fusionan bien con las herramientas de diferencias basadas en texto. Si varios commits cambian el mismo archivo binario e intentamos aplastarlos, puede producirse un conflicto porque Git no puede conciliar automáticamente estos cambios.
- Historia compleja: Si los commits tienen una historia compleja con múltiples fusiones, ramificaciones o rebases entre ellos, aplastarlos puede provocar conflictos debido a la naturaleza no lineal de los cambios.
Al aplastar, Git intentará aplicar cada cambio de uno en uno. Si encuentra conflictos durante el proceso, hará una pausa y nos permitirá resolverlos.
El conflicto se marcará con los marcadores de conflicto <<<<<<
y >>>>>>
. Para manejar los conflictos, tenemos que abrir los archivos y resolver manualmente cada uno seleccionando qué parte del código queremos conservar.
Después de resolver los conflictos, tenemos que escenificar los archivos resueltos utilizando el comando git add
. A continuación, podemos continuar el rebase utilizando el siguiente comando:
git rebase --continue
Para saber más sobre los conflictos en Git, consulta este tutorial sobre cómo resolver conflictos de fusión en Git.
Alternativas al Aplastamiento con Rebase
El comando git merge --squash
es un método alternativo a git rebase -i
para combinar varias confirmaciones en una sola. Este comando es especialmente útil cuando queremos fusionar los cambios de una rama en la rama principal aplastando todos los commits individuales en uno solo. Aquí tienes un resumen de cómo aplastar utilizando git merge
:
- Navegamos hasta la rama de destino en la que queremos incorporar los cambios.
- Ejecutamos el comando
git merge --squash
sustituyendopor el nombre de la rama.
- Confirmamos los cambios con
git commit
para crear una única confirmación que represente todos los cambios de la rama de características.
Por ejemplo, supongamos que queremos incorporar los cambios de la rama feature/login-form
en main
como una única confirmación:
git checkout main
git merge --squash feature-branch
git commit -m "Implement login form"
Éstas son las limitaciones de este enfoque en comparación con git rebase -i
:
- Granularidad del control: Menos control sobre los commits individuales. Con rebase podemos elegir qué commits fusionar, mientras que fusionar obliga a combinar todos los cambios en un solo commit.
- Historia intermedia: Al utilizar la fusión, el historial de confirmaciones individuales de la rama de características se pierde en la rama principal. Esto puede dificultar el seguimiento de los cambios incrementales realizados durante el desarrollo de la función.
- Revisión previa al compromiso: Dado que escenifica todos los cambios como un único conjunto de cambios, no podemos revisar ni probar cada confirmación individualmente antes de aplastarla, a diferencia de lo que ocurre durante un rebase interactivo, en el que cada confirmación puede revisarse y probarse en secuencia.
Conclusión
Incorporar commits frecuentes y pequeños al flujo de trabajo de desarrollo fomenta la colaboración y una documentación clara, pero también puede desordenar el historial del proyecto. Aplastar commits consigue un equilibrio, preservando los hitos importantes y eliminando al mismo tiempo el ruido de los cambios iterativos menores.
Para saber más sobre Git, te recomiendo estos recursos: