Prueba gratis
Engineering

Dos años haciendo squash merge

Simone Carletti's profile picture Simone Carletti on

En DNSimple utilizamos solicitudes de extracción todos los días como un flujo de trabajo estándar para proponer, revisar y enviar cambios a casi cualquier repositorio de Git. Para la mayoría de los repositorios centrales, como la aplicación web DNSimple o nuestra infraestructura basada en Chef, nuestra política es no comprometerse con el maestro, sino realizar cambios en una rama separada, abrir una solicitud de extracción, obtener una revisión de una o dos personas ( dependiendo del cambio), y luego combine la rama en el maestro antes de implementar.

Hace poco más de dos años, decidimos cambiar el flujo de trabajo del equipo de desarrollo para usar siempre git --squash merge. En esta publicación, resaltaré las razones de esta decisión, cómo funcionó para nosotros y cuáles son los beneficios.

git merge: avance rápido, recursivo y squash

Antes de entrar en los detalles de por qué adoptamos la fusión --squash, echemos un vistazo rápido a las estrategias de fusión más comunes en git. Nota: esta definitivamente no es una explicación completa del comando git merge. Para obtener explicaciones más detalladas, consulte la documentación de git merge. En primer lugar, el propósito de git merge es incorporar los cambios de otra rama a la actual. Para simplificar, supondremos que queremos fusionar una rama que contiene nuestros cambios llamada bugfix en la rama master.

avance rápido

Si master no se ha desviado de la rama, cuando sea el momento de fusionar, git simplemente moverá la referencia de master hacia la última confirmación de la rama bugfix.

C - D - E corrección de errores
/
Maestro A-B

Después de git merge:

A - B - C - D - E maestro/corrección de errores

Aquí está el resultado de la combinación:

➜ merge-examples git:(maestro) git merge --ff corrección de errores
Actualización de 9db2ac7..3452cab
Avance rápido

Esto se conoce como avance rápido.

Sin avance rápido

El comportamiento predeterminado de Git es utilizar el avance rápido siempre que sea posible. Sin embargo, es posible cambiar este comportamiento en la configuración de git o pasar la opción --no-ff (sin avance rápido) a git merge. Como resultado, incluso si git detecta que el maestro no divergió, creará una confirmación de fusión.

C - D - E corrección de errores
/
Maestro A-B

Después de git merge --no-ff:

C - D - E corrección de errores
/ \
A - B ------------ F maestro

Aquí está el resultado de la combinación:

➜ merge-examples git:(maestro) git merge --no-ff bugfix
¡Ya está actualizado!
Fusión realizada por la estrategia 'recursiva'.

Estrategia recursiva

Hasta ahora, asumimos que master nunca se separó de la rama bugfix. Sin embargo, esto es bastante improbable, incluso en un equipo pequeño con varios desarrolladores trabajando en varios cambios diferentes al mismo tiempo. Tome el siguiente ejemplo:

C - D - E corrección de errores
/
A - B - F - G maestro

Las confirmaciones F y G causaron que master divergiera de bugfix. Por lo tanto, git no puede simplemente avanzar rápidamente la referencia a E o perderá esas 2 confirmaciones. En este caso, git (generalmente) adoptará una estrategia de fusión recursiva. El resultado es una confirmación de combinación que une las dos historias:

C - D - E corrección de errores
/ \
A - B - F - G ----- H maestro

Aquí está el resultado de la combinación:

➜ merge-examples git:(maestro) git merge --no-ff bugfix
¡Ya está actualizado!
Fusión realizada por la estrategia 'recursiva'.

Fusión de calabaza

Squash merge es un enfoque de combinación diferente. Las confirmaciones de la rama fusionada se comprimen en una sola y se aplican a la rama de destino. Aquí hay un ejemplo:

C - D - E corrección de errores
/
A - B - F - G maestro

Después de git merge --squash && git commit:

C - D - E corrección de errores
/
A - B - F - G - Maestro CDE

donde CDE es una única confirmación que combina todos los cambios de C + D + E. Squashing retiene los cambios pero descarta todas las confirmaciones individuales de la rama bugfix. Tenga en cuenta que git merge --squash prepara la fusión pero en realidad no realiza una confirmación. Deberá ejecutar git commit para crear la confirmación de combinación. git ya ha preparado el mensaje de confirmación para que contenga los mensajes de todas las confirmaciones aplastadas.

¿Qué problema estamos tratando de resolver?

La razón principal por la que decidimos probar --squash merge fue para mejorar la calidad del historial de confirmaciones del repositorio. Las confirmaciones son esencialmente inmutables. Técnicamente, hay formas de reescribir la historia, pero hay varias razones por las que generalmente no quieres hacerlo. En aras de la simplicidad, digamos que cuanto más lejos está el compromiso en el historial del repositorio, más complicado es reescribirlo. Es importante escribir buenos compromisos porque son el pilar de su historial de git. Es difícil definir perfectamente qué hace que una confirmación sea una buena confirmación, pero en mi experiencia, una buena confirmación satisface al menos los siguientes requisitos:

  1. Combina todos los cambios de código relacionados con un solo cambio lógico (podría ser una función, una corrección de errores o un cambio individual que forma parte de un cambio mayor)
  2. Proporciona un mensaje de confirmación explicativo que ayuda a las personas a comprender la intención del cambio.
  3. Si elige este compromiso independientemente del historial, tiene sentido por sí solo El requisito uno debe ser su hábito de codificación predeterminado. Una confirmación debe representar un cambio atómico y debe evitar combinar varios cambios que no estén relacionados entre sí. Aunque esto parece obvio, he visto confirmaciones que cambian el script de compilación e introducen una nueva función en la aplicación. Usemos otro ejemplo más práctico: está corrigiendo un error, por lo que queremos que los cambios en el software se confirmen junto con las pruebas de regresión, no en diferentes confirmaciones que no estén relacionadas entre sí. El requisito dos es un problema muy conocido. Hay cientos de artículos que intentan definir un buen mensaje de confirmación y tratan de enseñar al programador el arte de escribir un buen mensaje de confirmación. La [página oficial de contribución de git] (https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project) tiene algunas pautas:
    Breve (50 caracteres o menos) resumen de cambios
    Texto explicativo más detallado, si es necesario. Envuélvelo para
    unos 72 caracteres más o menos. En algunos contextos, la primera
    línea se trata como el asunto de un correo electrónico y el resto de
    El texto como cuerpo. La línea en blanco que separa la
    resumen del cuerpo es fundamental (a menos que omita el cuerpo
    enteramente); herramientas como rebase pueden confundirse si ejecuta
    los dos juntos
    Los párrafos posteriores van después de las líneas en blanco.
    - Las viñetas también están bien
    - Por lo general, se usa un guión o un asterisco para la viñeta,
    precedida de un solo espacio, con líneas en blanco en
    entre, pero las convenciones varían aquí
    

    Estas pautas se extraen de un [artículo escrito por Tim Pope en 2008] (https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). Es probablemente el artículo más antiguo que recuerdo sobre este asunto. Así que hemos visto que escribir buenos mensajes de compromiso parece ser una regla difícil de seguir. También hemos visto que hay algunas métricas objetivas que puede seguir, y algunas herramientas aplican o fomentan estas métricas: Sin embargo, escribir un buen mensaje de compromiso es difícil porque no se trata solo de seguir métricas objetivas. Puede escribir un mensaje de confirmación perfectamente formateado pero completamente inútil: OK, uno aún más inútil: Levante la mano si alguna vez creó una confirmación con un mensaje Fix test, Fix CI, Change foo, Add bar. ¿Qué hay de malo en este mensaje de confirmación?, te estarás preguntando. Esto nos lleva al requisito tres. Una buena confirmación (y un buen mensaje de confirmación) es aquella que, si selecciona esa confirmación en cualquier momento de los cientos de confirmaciones en el repositorio, tendrá sentido por sí sola (o proporcionará suficiente información para reconstruir el motivo de la confirmación). el cambio). Hagamos un experimento. ¿Puedes decirme de qué se trata este cambio? De hecho, corrige algunas especificaciones con el objetivo de hacerlas pasar. Pero imagine si alguien se tropieza con los cambios en la línea 286 en algún momento dentro de 3 años. Ni el mensaje de confirmación ni el código explican por qué se rompieron las especificaciones, cuándo se rompieron, qué las rompió y por qué se requirió el cambio en la línea 286. De forma aislada, este compromiso no tiene sentido. Otro ejemplo común de mensajes no muy útiles es el primero de esta historia: Imagina que estás navegando por la lista de cientos de confirmaciones que intentan investigar cuándo y por qué se rompió algo. Creo que estaría de acuerdo en que el esfuerzo requerido para determinar si la primera confirmación podría ser candidata para examinar es mayor que la tercera confirmación. Desde el punto de vista del mensaje, requiere que (al menos) abra la confirmación para examinar los cambios. Además, la primera confirmación también puede romper el requisito uno, porque incluye varios cambios en la misma confirmación. Puede argumentar que actualizar múltiples dependencias es un solo cambio lógico, pero si ese es el caso, probablemente esté subestimando el impacto de cambiar incluso una sola dependencia en un proyecto grande.

    Ventajas de git squash merge

    Ahora que conocemos el problema, veamos cómo nos puede ayudar la combinación de squash. Como expliqué antes, el uso de squash merge agrupará todos los cambios en una sola confirmación, lo que también nos dará la oportunidad de escribir un mensaje de confirmación nuevo y completo que describa adecuadamente la intención de los cambios. El uso de mensajes de confirmación es una excelente manera de limitar la presencia de conjuntos de cambios aislados en su base de código. Mejora drásticamente la calidad del código que se encuentra en la rama del repositorio principal, lo que garantiza que solo estén presentes conjuntos de cambios independientes y autónomos. Este es un ejemplo de cómo se ve el historial de la aplicación DNSimple: En caso de que se pregunte si estamos perdiendo los cambios individuales, la respuesta es no. Cada combinación de squash hace referencia a un PR donde se rastrean todos los cambios: Ocasionalmente, se produce una fusión sin aplastamiento. Sucede. Todos somos seres humanos. Pero puedes ver inmediatamente la diferencia cuando esto sucede:

    Preguntas e inquietudes

    El uso de squash merge ciertamente no es la única forma posible de mantener limpio y legible el historial de control de versiones. Hay una serie de mejores prácticas que cada desarrollador puede adoptar, individualmente o como equipo. Sin embargo, encontramos que esta característica proporciona el mejor equilibrio entre simplicidad, libertad y resultados. Si ha estado leyendo todo el camino hasta este punto, ciertamente tiene preguntas o comentarios. Aquí está el más común que escuché:

    ¿No estás desalentando la calidad del compromiso individual?

    No. Todavía se alienta a cada autor de confirmación a escribir buenas confirmaciones: combinar cambios significativos, junto con mensajes de confirmación explicativos. Sin embargo, no existe la presión de los compañeros de que un error tipográfico, un archivo faltante o una especificación rota terminen abarrotando la rama principal final.

    ¡Podrías usar git rebase!

    Sí, ciertamente podemos usar rebase para modificar un mensaje de confirmación o recombinar confirmaciones. Si bien esto puede funcionar para confirmaciones locales (y lo hago con frecuencia), se desaconseja reescribir el historial de git una vez que lo haya compartido (por ejemplo, después de enviarlo al repositorio compartido remoto). De hecho, para evitar problemas con los compañeros de equipo y las herramientas de integración continua, prohibimos explícitamente cambiar la base de sus compromisos después de que los haya enviado. Tampoco permitimos git push --force. El único caso de uso para --force o git rebase es en el raro caso de problemas graves que puedan comprometer la seguridad o la estabilidad del repositorio. Pero ese es un caso de uso excepcional.

    Puede usar ramas de corta duración para evitar la fusión repetitiva del maestro

    No, esto no funciona. Lo hace si tiene muy pocos desarrolladores, cada uno trabajando en ramas individuales. Pero cuando varios desarrolladores trabajan juntos en ramas de funciones múltiples, eso no se escala. Recomendamos que realice backporting master a menudo en su sucursal para limitar el riesgo de conflictos y mantenerse al tanto de los últimos cambios. Por ejemplo, actualizamos continuamente las dependencias. También fusionamos y enviamos en promedio 10 veces al día.

    Conclusión

    Al usar squash merge, hemos podido mejorar drásticamente la calidad de nuestro historial de cambios, convirtiendo nuestro registro de confirmación en una herramienta muy poderosa para navegar:

  4. Redujimos (y en ciertos casos incluso eliminamos) el número de confirmaciones fixme, fix before commit, fix specs en el repositorio. Los errores ocurren y el desarrollador tiene total libertad para experimentar e incluso realizar cambios incompletos en una rama con la plena confianza de que, una vez combinados, solo se mostrará el resultado final.
  5. El desarrollo de ramas de funciones ahora es mucho más fácil. Podemos elegir, retroadaptar e incluso fusionar periódicamente el maestro en una rama de desarrollo, sin preocuparnos por todas las fusiones recursivas que aparecen en los registros y saturan el historial.
  6. Aumentamos la confianza de los no desarrolladores o miembros del equipo no técnicos para contribuir. En particular, podemos aprovechar el uso de la interfaz de usuario web para editar archivos en el lugar. La fusión final todavía está sujeta a nuestro proceso de revisión por pares, y es responsabilidad del miembro líder fusionar el conjunto de cambios final con un mensaje apropiado. Un gran ejemplo es nuestro proceso actual de edición de copias, como se muestra en la siguiente captura de pantalla:
  7. Simplificamos el proceso de retroceder o revertir empaquetando todos los cambios en un conjunto único al final del historial de git.
  8. Facilitamos todas las tareas que requieren navegar por compromisos históricos (p. ej., a través de git culp), como la depuración, las revisiones y la limpieza de la deuda técnica. La razón principal es que todos los cambios relacionados con una función en particular no se incluyen en el mismo conjunto de cambios. Si "culpa" a una línea de código en particular y va a la diferencia de confirmación, obtendrá exactamente todos los cambios asociados, incluidos: métodos que se crearon, archivos de especificaciones que se tocaron, vistas que se actualizaron, etc. No más casos donde investiga por qué se cambió la firma de un método, y los únicos cambios que ve en la confirmación son la edición de la firma del método y (con suerte) la especificación correspondiente. Todos estos beneficios dan como resultado un código más fácil de mantener, menos tiempo dedicado a perseguir a los miembros del equipo para obtener información sobre por qué se cambió esa línea y más productividad con menos estrés.

Share on Twitter and Facebook

Simone Carletti's profile picture

Simone Carletti

Italian software developer, a PADI scuba instructor and a former professional sommelier. I make awesome code and troll Anthony for fun and profit.

We think domain management should be easy.
That's why we continue building DNSimple.

Try us free for 30 days
4.5 stars

4.3 out of 5 stars.

Based on Trustpilot.com and G2.com reviews.