“Comete pronto, comete a menudo” es un mantra popular en el desarrollo de software al usar Git. Hacerlo así asegura que cada cambio esté bien documentado, mejora la colaboración y facilita rastrear la evolución del proyecto. Sin embargo, esto también puede llevar a una sobrecantidad de commits.
Es aquí donde entra la importancia de combinar commits. Combinar commits es el proceso de unir múltiples entradas de commit en un solo commit coherente.
Por ejemplo, digamos que estamos trabajando en una funcionalidad que implementa un formulario de inicio de sesión, y creamos los siguientes cuatro commits:
Una vez que se completa la funcionalidad para el proyecto general, estos commits son demasiado detallados. No necesitamos saber en el futuro que nos topamos con un error que se fijó durante el desarrollo. Para garantizar un historial limpio en la rama principal, estos commits se combinan en un solo commit:
Cómo Combinar Commits en Git: Rebase Interactivo
El método más común para combinar commits es utilizando un rebase interactivo. Lo iniciamos usando el siguiente comando:
git rebase -i HEAD~<number_of_commits>
Reemplace <número_de_commits>
con el número de commits que desea combinar.
En nuestro caso, tenemos cuatro commits, por lo que el comando es:
git rebase -i HEAD~4
Ejecutar este comando abrirá un editor de línea de comandos interactivo:
La sección superior muestra los commits, mientras que la inferior contiene comentarios sobre cómo combinar commits.
Vemos cuatro commits. Para cada uno, debemos decidir qué comando ejecutar. Nos preocupan los comandos pick
(p
) y squash
(s
). Para combinar estos cuatro commits en uno solo, podemos seleccionar el primero y combinar los tres restantes.
Aplicamos los comandos modificando el texto que precede cada commit, específicamente cambiando pick
a s
o squash
para los segundos, terceros y cuartos commits. Para hacer estas ediciones, necesitamos ingresar al modo “INSERTAR” en el editor de texto de línea de comandos presionando la tecla i
en el teclado:
Después de presionar i
, aparecerá el texto -- INSERTAR --
en la parte inferior, indicando que hemos entrado en 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:
Una vez que estamos satisfechos con los cambios, necesitamos salir del modo de inserción presionando la tecla Esc
del teclado. El siguiente paso es guardar nuestros cambios y salir del editor. Para hacerlo, primero presionamos 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 indica que insertemos un comando. Para guardar los cambios, usamos el comando w
, que significa “escribir”. Para cerrar el editor, usamos q
, que significa “salir”. Estos comandos se pueden combinar y escribir juntos wq
:
Para ejecutar el comando, presionamos la tecla Enter
. Esta acción cerrará el editor actual y abrirá uno nuevo, permitiéndonos ingresar el mensaje de confirmación para el commit recién compactado. El editor mostrará un mensaje predeterminado que comprende los mensajes de los cuatro commits que estamos compactando:
a
Recomiendo modificar el mensaje para reflejar con precisión los cambios implementados por estas combinaciones de commits — después de todo, el propósito de unir commits es mantener un historial limpio y fácil de leer.
Para interactuar con el editor y editar el mensaje, presionamos nuevamente i
para ingresar al modo de edición y editamos el mensaje a nuestro gusto.
En este caso, reemplazamos el mensaje del commit con “Implementar formulario de inicio de sesión”. Para salir del modo de edición, presionamos Esc
. Luego guardamos los cambios presionando :
, ingresando el comando wq
y presionando Enter
.
Cómo ver el historial de commits
Generalmente, recordar todo el historial de commits puede ser desafiante. Para ver el historial de commits, podemos usar el comando git log
. En el ejemplo mencionado, antes de realizar el squash, ejecutar el comando git log
mostraría:
Para navegar por la lista de commits, utiliza las teclas de flecha hacia arriba y hacia abajo. Para salir, presiona q
.
Podemos usar git log
para confirmar el éxito del squash. Ejecutarlo después del squash mostrará un solo commit con el nuevo mensaje:
Pushing squashed commit
El comando anterior actuará en el repositorio local. Para actualizar el repositorio remoto, necesitamos empujar nuestros cambios. Sin embargo, debido a que hemos cambiado el historial de commits, necesitamos hacer un empuje forzado utilizando la opción --force
:
git push --force origin feature/login-form
El empuje forzado sobrescribirá el historial de commits en la rama remota y podría interrumpir a otros que están trabajando en esa rama. Es una buena práctica comunicarse con el equipo antes de hacer esto
Una forma más segura de hacer un push forzado, que reduce el riesgo de interrumpir a los colaboradores, es usar en su lugar la opción --force-with-lease
:
git push --force-with-lease origin feature/login-form
Esta opción asegura que solo hacer un push forzado si la rama remota no ha sido actualizada desde nuestra última fetch o pull.
Achatar específicos commits
Imagina que tenemos cinco commits:
Supongamos que queremos mantener los commits 1, 2 y 5 y achatar los commits 3 y 4.
Al usar rebase interativo, los commits marcados para achatar se combinarán con el commit directamente anterior. En este caso, significa que queremos achatar el Commit4
para que se fusione en el Commit3
.
Para hacer esto, debemos iniciar una rebase interactiva que incluya estos dos commits. En este caso, tres commits son suficientes, por lo que usamos el comando:
git rebase -i HEAD~3
Luego, configuramos Commit4
a s
para que se combine con Commit3
:
Después de ejecutar este comando y listar los commits, observamos que los commits 3 y 4 han sido combinados mientras que el resto permanecen sin cambios.
Combinar desde un commit específico
En el comando git rebase -i HEAD~3
, la parte HEAD
es un atajo para el commit más reciente. La sintaxis ~3
se usa para especificar un antepasado de un commit. Por ejemplo, HEAD~1
se refiere al padre del commit HEAD
.
En una rebase interactiva, los commits considerados incluyen todos los commits ancestros que llevan hasta el commit especificado en el comando. Nota que el commit especificado no se incluye:
En lugar de usar HEAD, podemos especificar directamente un hash de commit. Por ejemplo, Commit2
tiene un hash de dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
, por lo que el comando:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
iniciaría una rebase considerando todos los commits realizados después de Commit2
. Por lo tanto, si queremos iniciar una rebase en un commit específico e incluir ese commit, podemos usar el comando:
git rebase -i <commit-hash>~1
Resolución de Conflictos al Combinar Commits
Cuando combinamos commits, unimos varios cambios de commit en un solo commit, lo que puede llevar a conflictos si los cambios se superponen o divergen significativamente. Aquí hay algunos escenarios comunes en los que pueden surgir conflictos:
- Cambios superpuestos: Si dos o más confirmaciones que se van a combinar han modificado las mismas líneas de un archivo o líneas estrechamente relacionadas, Git puede no ser capaz de reconciliar automáticamente estos cambios.
- Estado de cambios diferentes: Si una confirmación añade un trozo de código y otra confirmación modifica o elimina ese mismo trozo de código, combinar estas confirmaciones puede llevar a conflictos que necesitan ser resueltos.
- Cambiar nombre y modificar: Si una confirmación cambia el nombre de un archivo y confirmaciones posteriores realizan cambios en el nombre antiguo, combinar estas confirmaciones puede confundir a Git, causando un conflicto.
- Cambios en archivos binarios: Los archivos binarios no se fusionan bien utilizando herramientas de diff basadas en texto. Si varios commits modifican el mismo archivo binario y tratamos de combinarlos, puede ocurrir un conflicto porque Git no puede reconciliar automáticamente estos cambios.
- Historial complejo: Si los commits tienen un historial complejo con múltiples fusiones, ramificaciones o rebase entre ellos, combinarlos puede resultar en conflictos debido a la naturaleza no lineal de los cambios.
Al combinar, Git intentará aplicar cada cambio uno a uno. Si encuentra conflictos durante el proceso, se detendrá y nos permitirá resolverlos.
El conflicto se marcará con marcadores de conflicto <<<<<<
y >>>>>>
. Para manejar los conflictos, necesitamos abrir los archivos y resolver manualmente cada uno seleccionando qué parte del código queremos mantener.
Después de resolver los conflictos, necesitamos preparar los archivos resueltos utilizando el comando git add
. Luego podemos continuar el rebase usando el siguiente comando:
git rebase --continue
Para obtener más información sobre los conflictos en Git, consulte este tutorial sobre cómo resolver conflictos de fusión en Git.
Alternativas a la Compresión con Rebase
El comando git merge --squash
es un método alternativo al git rebase -i
para combinar varios commits en uno solo. Este comando es especialmente útil cuando queremos fusionar cambios de una rama en la rama principal mientras aplastamos todos los commits individuales en uno. Aquí tienes una descripción general de cómo aplastar usando git merge
:
- Navegamos a la rama de destino en la que queremos incorporar los cambios.
- Ejecutamos el comando
git merge --squash <branch-name>
reemplazando<branch-name>
con el nombre de la rama. - Commitamos los cambios con
git commit
para crear un solo commit que represente todos los cambios de la rama de características.
Por ejemplo, digamos que queremos incorporar los cambios de la rama feature/login-form
en main
como un solo commit:
git checkout main git merge --squash feature-branch git commit -m "Implement login form"
Estas son las limitaciones de este enfoque en comparación con git rebase -i
:
- Granularidad de control: Menos control sobre los commits individuales. Con rebase podemos elegir qué commits combinar, mientras que el merge fuerza combinar todos los cambios en un solo commit.
- Historia intermedia: Al usar merge, la historia individual de los commits de la rama de características se pierde en la rama principal. Esto puede dificultar rastrear los cambios incrementales que se realizaron durante el desarrollo de la característica.
- Revisión pre-commit: Dado que stages todos los cambios como un solo conjunto de cambios, no podemos revisar o probar cada commit individualmente antes de squashing, a diferencia de durante una rebase interactiva donde cada commit puede ser revisado y probado en secuencia.
Conclusión
Incorporar commits frecuentes y pequeños en el flujo de trabajo de desarrollo promueve la colaboración y la documentación clara, pero también puede desordenar la historia del proyecto. Aplastar commits logra un equilibrio, preservando los hitos importantes mientras se elimina el ruido de los cambios iterativos menores.
Para aprender más sobre Git, recomiendo estos recursos:
Source:
https://www.datacamp.com/tutorial/git-squash-commits