Qué tiene en común el guardarropa de Barack Obama con una buena historia de Git

Un día de verano en el calor de finales de agosto de 2014, el entonces presidente Barack Obama tomó una decisión que conmocionaría a la nación: se puso un traje diferente. La “ controversia del traje bronceado ” resultante dominó un ciclo de noticias y se difundió por una variedad de razones, pero finalmente fue desencadenada por la novedad del traje en sí. Al igual que los cuellos de tortuga negros de Steve Jobs y las camisas grises de Mark Zuckerberg, Obama solía quedarse con los mismos trajes azules o grises todos los días.
El denominador común detrás de este comportamiento compartido es el concepto psicológico de fatiga de decisión : que incluso las decisiones más pequeñas que tomamos cada día pueden agotar la cantidad finita de capacidad intelectual que tenemos para tomar decisiones y hacerlo bien. Una estrategia adoptada por estas personas para preservar este preciado recurso es eliminar tantas decisiones menores como sea posible: usar las mismas cosas, comer las mismas cosas, seguir el mismo horario, etc. Esto le permite enfocar su energía mental en las decisiones que realmente importan.
Entonces, ¿qué tiene que ver todo esto con el sistema de control de versiones favorito de todos, git? Como ocurre con muchas cosas en la programación, no existe una forma "correcta" de estructurar una confirmación de git o administrar el historial de git de un proyecto; simplemente debe elegir un principio rector y organizar sus patrones en torno a él. Personalmente, creo en elegir una estrategia que reduzca la fatiga de decisión (y la "fatiga mental" en términos más generales) para todos los "consumidores" de una confirmación. Voy a entrar en más detalles sobre cómo hacer esto a continuación (y no dude en saltar a esa lista numerada si lo desea), pero creo que es muy importante abordar primero por qué creo esto. Y debemos comenzar con para quién es realmente un compromiso.
¿De quién es la historia?
Debe enfatizarse que en el transcurso del ciclo de vida completo de un proyecto promedio, la cantidad de personas que ven un compromiso que no lo escribió superará con creces a las que sí lo hicieron. Estas otras personas también tendrán el conocimiento menos íntimo de para qué sirve realmente un compromiso y cómo se pretende que funcione. Entonces, como cuestión práctica, construir un buen historial de git debería ser realmente para ellos . Y, con el tiempo suficiente, incluso el código que usted mismo escribió algún día puede parecerle extraño. Por lo tanto, mantener un buen historial también puede ayudar a su yo futuro.
Teniendo esto en cuenta, vale la pena señalar que hay dos amplias categorías de personas que, en algún momento, verán una confirmación en particular:
- Revisores de código
Aquellos que ven la confirmación antes de que se fusione con el historial a través del proceso de revisión de código. Estas personas son generalmente conocidas como "revisores de código". En un buen equipo, todos, en algún momento, serán revisores de código y puede haber varios para cada conjunto de nuevos cambios de código. - Detectives de código
Aquellos que ven la confirmación después de que se haya fusionado. Por lo general, son personas que revisan el historial para tratar de comprender por qué se agregó algo o cuándo se introdujo un error. A falta de un nombre mejor, llamaré a estas personas "detectives de código" para distinguirlos de las personas de arriba.
Los detectives de códigos tienen todos estos mismos desafíos además de otros: es posible que no siempre sepan lo que están buscando e incluso cuando lo encuentran, pueden carecer de un contexto vital para comprenderlo. Muchas veces ni siquiera tienen el beneficio de poder hablar con el autor original del código. Por esta razón, una gran parte de lo que hace un detective de código es tratar de inferir la intención del código existente sin poder preguntar al respecto o dar seguimiento a sus sospechas.
Los trabajos de estos dos grupos son ligeramente diferentes, pero en esencia ambos implican una serie de decisiones que deben tomarse, línea por línea, para responder a la última pregunta: ¿ qué es lo que hace este código? Dependiendo de cómo el autor estructure las confirmaciones, esto puede ser relativamente sencillo o una tarea dolorosa con obstáculos innecesarios y pistas falsas.
Decisiones decisiones
Consideremos ahora qué tipo de decisiones se toman para comprender un compromiso. Primero, debemos tener en cuenta que cada línea de una confirmación se puede categorizar como una de dos cosas: una línea de código "agregada" o una "eliminada".
Sin ningún contexto adicional, se deben tomar las siguientes decisiones al ver una sola línea de código "agregado":
- ¿Es esta una línea de código completamente nueva?
- Si no es una nueva línea de código, ¿es una línea de código existente que simplemente se ha movido de otro lugar?
- Si no es una nueva línea de código y no se ha movido, ¿es una modificación trivial de una línea existente (como un cambio de formato) o es un cambio lógico legítimo?
- Si es una línea de código completamente nueva o una modificación que lleva a un cambio lógico, ¿por qué se hace? ¿Está hecho correctamente? ¿Se puede simplificar o mejorar?
Podemos ver un proceso similar para cada línea de código "eliminada":
- ¿Esta línea se eliminará por completo?
- Si no se elimina por completo, ¿se está moviendo o modificando?
- Si no se elimina por completo y no solo se mueve, ¿es el resultado de una modificación trivial (por ejemplo, el formateo) o el resultado de un cambio lógico?
- Si de hecho es una modificación lógica, ¿por qué se modifica? ¿Se está haciendo correctamente?
- Si la línea se elimina por completo, ¿por qué ya no es necesaria?
Así que esto finalmente nos lleva de vuelta a la fatiga de decisión:
¿Cómo podemos organizar las confirmaciones para eliminar estas primeras opciones triviales y permitir que los espectadores se centren en las importantes?
No desea que su equipo dedique su capacidad mental y su tiempo limitados a decidir, por ejemplo, que una parte del código se movió de un módulo a otro sin modificaciones y luego se pierda los errores de codificación presentes en el nuevo código real. Multiplique esto entre los equipos de una gran organización y esto puede sumar una pérdida de productividad medible.
Entonces, ahora que hemos discutido por qué creo que debemos seguir esta estrategia, analicemos finalmente cómo abogo por ponerla en práctica.
1. Coloque modificaciones triviales en sus propios compromisos
Lo más simple e importante que se puede hacer es separar las modificaciones triviales en sus propios compromisos. Algunos ejemplos de esto incluyen:
- Cambios de formato de código
- Cambios de nombre de función/variable/clase
- Reordenación de funciones/variables/importaciones dentro de una clase
- Eliminando código no utilizado
- Mover ubicaciones de archivos
Considere la siguiente confirmación que mezcla cambios triviales con otros no triviales:
Confirmar mensaje: "Actualizar lista de frutas válidas"

¿ Cuánto tiempo le llevó detectar los cambios no triviales ? Ahora vea lo que sucede cuando estos dos cambios se dividen en dos confirmaciones separadas:
Confirmar mensaje: "Actualizar formato de lista de frutas válido"

Confirmar mensaje: "Agregar fechas a la lista de frutas válidas"

La confirmación de "solo formato" se puede ignorar esencialmente y las adiciones de código se pueden descubrir inmediatamente de un vistazo.
2. Coloque refactores de código en sus propias confirmaciones
Los refactores de código implican cambios en la estructura de algún código, pero no en su función. A veces esto se hace por sí mismo, pero a menudo se hace por necesidad: para construir sobre el código existente, a veces es necesario refactorizarlo primero y luego puede ser tentador hacer ambas cosas a la vez. Sin embargo, se pueden cometer errores durante una refactorización y se requiere especial cuidado al revisarlos. Al colocar este código en su propio compromiso claramente indicado como refactor, el revisor sabe marcar cualquier desviación del comportamiento lógico existente como un posible error.
Por ejemplo, ¿qué tan rápido puede detectar el error aquí?
Confirmar mensaje: "Actualizar lógica de sugerencia"

¿Qué tal ahora con el refactor dividido?
Confirmar mensaje: "Extraer tasa de propina predeterminada"

Confirmar mensaje: "Permitir tasas de propinas personalizadas"

3. Coloque las correcciones de errores en sus propias confirmaciones
A veces, al realizar cambios en el código, observa un error en el código existente que desea modificar o desarrollar. Con el interés de seguir adelante, puede corregir ese error e incluirlo en sus cambios que de otro modo no estarían relacionados en el mismo compromiso. Cuando se mezcla de esta manera hay varias complicaciones:
- Es posible que otras personas que vean este código no sepan que se está solucionando un error.
- Incluso cuando se sabe que se incluye una corrección de errores, puede ser difícil saber qué código fue parte de la corrección del error y cuál fue parte de los otros cambios lógicos.
4. Coloque cambios lógicos separados en sus propias confirmaciones
Después de dividir los tipos de cambios anteriores, ahora debería quedarse con una sola confirmación con cambios lógicos y legítimos para agregar/actualizar/eliminar funcionalidad. Para un cambio pequeño y conciso, esto suele ser suficiente. Sin embargo, a veces este compromiso agrega una característica completamente nueva (con pruebas) y registra más de 1000 líneas (o más). Git no presentará estos cambios de manera coherente y comprender con éxito este código requeriría que el revisor salte y mantenga una gran fracción de estas líneas en la memoria a la vez para seguir. Junto con la fatiga de decisión que implica el procesamiento de cada línea, estirar su memoria de trabajo de esta manera es mentalmente agotador y, en última instancia, ineficiente.
Siempre que sea posible, divida las confirmaciones en función de los dominios de modo que cada confirmación se compile de forma independiente. Esto significa que se puede agregar primero el código más independiente, seguido del código que depende de él, y así sucesivamente. El código bien estructurado debería dividirse de esta manera con bastante naturalidad, mientras que las dificultades encontradas en esta etapa podrían indicar problemas estructurales más grandes, como dependencias circulares. Este ejercicio puede incluso conducir a mejoras en el propio código.
5. Combinar cualquier cambio de revisión en las confirmaciones a las que pertenecen
Después de dividir su trabajo en varias confirmaciones limpias, es posible que reciba comentarios de revisión que requieran que realice cambios en el código que aparece en una o más de ellas. Algunos desarrolladores reaccionarán a estos comentarios agregando nuevas confirmaciones que aborden estas inquietudes. La lista de confirmaciones en un PR dado podría comenzar a parecerse a lo siguiente:
- <Initial commits>
- Respond to review feedback
- Work
- More work
- Addressing more review feedback
Lo mismo ocurre cuando se abre por primera vez una solicitud de extracción: cada confirmación debe tener un propósito y no debe ser negada por cambios en confirmaciones posteriores por las mismas razones mencionadas anteriormente.
Considere este cambio inicial seguido de varias confirmaciones de "trabajo":




Ahora imagine que está viendo estos cambios bien avanzado el proceso (o incluso años después). ¿No le gustaría ver lo siguiente?

6. ¡Rebase, rebase, rebase!
Si una rama de función ha existido durante el tiempo suficiente, ya sea por el tiempo que lleva agregar el código inicial o por un largo proceso de revisión del código, puede comenzar a entrar en conflicto con los cambios realizados en la rama principal en la que se basó originalmente. Ahora hay dos formas de hacer que la rama de funciones sea actual:
- Combinar la rama principal en la rama característica. Esto generará una "confirmación de combinación" en la que se incluyen todos los cambios de código necesarios para abordar los conflictos. Si la rama de función es particularmente antigua, este tipo de confirmaciones pueden ser sustanciales.
- Rebase la rama de características contra la rama principal. El producto final aquí es un nuevo conjunto de confirmaciones que actúan como si acabaran de crearse en función de la rama principal actualizada. Cualquier conflicto deberá tratarse como parte del proceso de cambio de base, pero desaparecerá toda evidencia de la versión original del código.
Si le importa producir un historial limpio (¡y debería hacerlo!), la reorganización es la mejor opción aquí: todos los cambios se construyen entre sí de manera ordenada y lineal. No necesita herramientas sofisticadas de visualización de historial para comprender la relación entre las ramas.
Considere el siguiente historial de proyectos que emplea la fusión entre sucursales:

Compare esto con un proyecto que vuelve a basar todos los cambios y prohíbe las confirmaciones de fusión incluso cuando se fusionan funciones en la rama principal :

En el primero se deben graficar, ponderar y descifrar las relaciones entre los cambios; en el último, simplemente fluyes hacia adelante y hacia atrás en el tiempo.
Algunos pueden argumentar que en realidad es el rebasado lo que destruye la historia; que pierde el historial de cambios realizados para obtener un código en su forma final antes de fusionarse. Pero este tipo de historial rara vez es útil y depende mucho del desarrollador: el viaje de una persona puede diferir del siguiente, pero lo que importa es ver una serie de confirmaciones en el historial que reflejen los cambios finales que representan... cualquier proceso que sea necesario para obtener allá. Sí, hay casos especiales aquí donde las confirmaciones de combinación son inevitables, pero deberían ser la excepción. Y, a menudo, los escenarios que los provocan (como las ramas de funciones de larga duración compartidas por varios miembros del equipo) se pueden evitar mediante mejores flujos de trabajo (como el uso de indicadores de funciones en lugar de ramas de funciones compartidas ).
Contra argumentos
Ciertamente, se pueden presentar argumentos en contra de este enfoque y he tenido muchas discusiones con personas que no están de acuerdo con él. Estos puntos no carecen de mérito y, como mencioné al comienzo del artículo, no existe una forma "correcta" de estructurar compromisos. Quiero resaltar rápidamente algunos puntos que he escuchado y dar mi opinión sobre cada uno.
"Preocuparse tanto por la estructura de confirmación ralentiza el desarrollo".
Este es uno de los puntos más comunes que he escuchado en contra de este enfoque. Sin duda, es cierto que el desarrollador tardará un poco más de tiempo en escribir el código para considerar cuidadosamente y dividir los cambios. Sin embargo, esto también se aplica a cualquier otro tipo de procesos adicionales destinados a proteger contra las debilidades inherentes de priorizar la velocidad y, a la larga, es posible que no le ahorre tiempo al equipo en general. Por ejemplo, los equipos que no escriben pruebas unitarias utilizan el argumento de que el desarrollo se ralentizará, pero estos mismos equipos necesitan dedicar más tiempo a corregir el código roto y probar manualmente los refactores. Y, una vez que un equipo tiene el hábito de dividir sus cambios de esta manera, el tiempo adicional agregado se reduce considerablemente porque se convierte en parte del proceso de desarrollo normal.
"Mi proyecto utiliza herramientas que ni siquiera permiten cambios de formato triviales".
Estoy de acuerdo en que esta es una excelente manera de minimizar el daño que de otro modo causaría la rotación de código relacionada con el formato. Como desarrollador de Android, creo firmemente en el uso de formateadores automáticos en todo el equipo y apuesto por herramientas como ktlint . Sin embargo, también sé de primera mano por la configuración de todas estas herramientas que no son perfectas y que hay muchos posibles cambios de formato sobre los que son totalmente indiferentes. Y, como se discutió anteriormente, algunos cambios triviales no son simplemente cambios de formato, como reordenar el código. Siempre se pueden realizar cambios de código triviales y, por lo tanto, debe haber un plan sobre la mejor manera de manejarlos.
"No todos los sitios de alojamiento de git permiten solicitudes de extracción con múltiples confirmaciones".
¡Esto es muy cierto! Mis recomendaciones se basan principalmente en el uso de herramientas como GitHub y GitLab que permiten que un RP tenga tantas confirmaciones como desee, pero hay herramientas como Gerrit que no las permiten. En este caso, solo considere cada compromiso como su propio PR. Esto introduce aún más gastos generales para el autor (ya veces para los revisores), pero creo que a la larga vale la pena el esfuerzo. Incluso puede haber formas de agilizar este proceso y relacionar estos RP separados entre sí, como usar "cambios dependientes" en Gerrit.
"Una sola confirmación garantiza que todos los cambios se compilen y pasen las pruebas".
Este también es un muy buen punto. Las comprobaciones automáticas que se ejecutan en los sitios de alojamiento de git generalmente solo se ejecutan en el conjunto completo de cambios, no para cada confirmación individual. Si hay una confirmación rota en el camino que se soluciona con cambios posteriores, no hay forma de detectarlo automáticamente. Desea que cada compromiso pueda valerse por sí mismo en caso de que algún día necesite regresar y probar el estado del código en ese punto para rastrear errores, etc. Como regla comprensible, debe ser obligatorio para cada compromiso en un PR de confirmación múltiple para compilar y aprobar cualquier prueba relevante, pero no hay forma de hacer cumplir esto estrictamente (aparte de hacer que cada confirmación sea su propia PR). Esto requiere vigilancia, pero es algo que debe sopesarse frente a los beneficios que se obtienen al dividir el código.
"Una sola confirmación proporciona el mayor contexto para todos los cambios".
Este es un punto interesante. Si bien los sitios de alojamiento de git como GitHub permiten agregar comentarios de forma masiva a un grupo de confirmaciones como parte de una descripción de relaciones públicas, tal cosa no existe en git. Esto significa que el vínculo entre compromisos agregados en el mismo PR no es estrictamente parte de la historia. Afortunadamente, los sitios como GitHub tienen características que agregan un enlace al PR que produjo un compromiso al verlo de forma aislada:

Si bien esto no es tan útil como tener este enlace en el historial de git, para muchos proyectos es una forma adecuada de realizar un seguimiento de la relación entre las confirmaciones.
Pensamientos finales
Espero haberlo convencido de que dividir los cambios de código en confirmaciones distintas de varios tipos tiene beneficios para todos en el proceso de desarrollo:
- Puede ayudar al escritor a mejorar la estructura del código y transmitir mejor el contenido de los cambios.
- Puede ayudar a los revisores de código a revisar el código más rápidamente y reducir la fatiga mental al permitirles centrar su atención en cambios separados y significativos.
- Puede ayudar a cualquier persona que mire hacia atrás en el historial del código a encontrar cambios lógicos y errores más rápidamente y, de manera similar, reducir la carga mental que conlleva hojear grandes cantidades de historial.
Brian trabaja en Livefront , donde siempre intenta hacer un poco más (git) de historia.