Git 2.56 llega con conflictos más seguros y rendimiento optimizado

Git, el sistema de control de versiones distribuido de código abierto, ha publicado su versión 2.56.0. Este lanzamiento ha contado con la participación de más de un centenar de desarrolladores, de los cuales una treintena se estrena como contribuyentes al proyecto. La nueva entrega incluye un conjunto de mejoras que afectan a tareas cotidianas como la resolución de conflictos, la optimización de operaciones en repositorios grandes y la gestión de referencias.
A lo largo de las próximas líneas repasamos las novedades más relevantes que trae esta actualización. Muchas de ellas están pensadas para facilitar el trabajo diario de los mantenedores y para reducir el tiempo de ejecución en repositorios de gran tamaño, un aspecto crítico en entornos de desarrollo profesionales.
Un modo más seguro para marcar conflictos resueltos en Git 2.56
Cuando se produce un conflicto durante una fusión, el proceso tiene dos fases: primero se edita el árbol de trabajo hasta que cada archivo en conflicto queda como se desea, y después se preparan esas rutas para indicar a Git que el conflicto está solucionado. El problema es que comandos como git add -u actualizan todas las rutas modificadas que están bajo seguimiento, lo que puede incluir cambios locales ajenos al conflicto o incluso archivos que aún contienen marcadores de conflicto.
Para evitar estos descuidos, Git 2.56 incorpora git add --resolved. Este modo considera únicamente las rutas que actualmente están sin fusionar en el índice. Antes de preparar nada, revisa los archivos regulares no fusionados en busca de marcadores de conflicto residuales. Si encuentra alguno, informa de las rutas afectadas y deja el índice intacto. Además, se le puede pasar un patrón de rutas para limitar qué archivos no fusionados se tienen en cuenta, y la comprobación de marcadores se aplica de forma global a la selección: si algún archivo seleccionado contiene marcadores, no se prepara ninguno. Las eliminaciones resueltas y los conflictos binarios, al no tener marcadores textuales, sí se pueden preparar con normalidad.
Esta opción es deliberadamente más restrictiva que git add -u o git add -A, no se puede combinar con ellas e ignora los archivos bajo seguimiento que nunca estuvieron en conflicto. Resulta útil como red de seguridad en flujos de trabajo donde una fusión puede comenzar con cambios locales no relacionados ya presentes.
El algoritmo de merge-base deja de buscar antes
Muchas operaciones de Git necesitan localizar el mejor ancestro común de dos confirmaciones. Las fusiones los usan como punto de partida para combinar cambios, y las diferencias de tres puntos emplean un ancestro común para decidir dónde empieza el trabajo de una rama temática. Los servidores de repositorios realizan el mismo cálculo para diferencias de solicitudes de extracción, comprobaciones de fusionabilidad y rangos de revisión.
Git encuentra estos ancestros caminando hacia atrás desde ambos extremos. Imagina que se pintan de azul las confirmaciones alcanzables desde un lado y de rojo las del otro. Una confirmación alcanzada por ambos colores es candidata a base de fusión. Encontrar un candidato no siempre es suficiente: las fusiones cruzadas pueden producir múltiples bases de fusión, ninguna de las cuales es ancestro de otra, por lo que Git debe seguir caminando hasta saber que las ha encontrado todas. La regla de parada anterior podía seguir procesando una larga cola de historia obsoleta y ya común después de que otra base de fusión hubiera dejado de ser posible.
La versión 2.56 lleva la cuenta de cuántas confirmaciones en cola quedan pintadas exclusivamente por cada lado. Una vez que un lado exclusivo se agota, no puede aparecer un nuevo punto de encuentro, así que Git puede detenerse sin dejar de devolver todas las bases de fusión. La diferencia puede ser notable cuando ramas laterales antiguas se han fusionado en una historia mucho más larga. En un caso real de un monorepositorio, el recorrido pasó de 0,68 segundos a 0,01 segundos. Las evaluaciones en producción sobre dos monorepositorios grandes encontraron muchos casos alrededor de 70 veces más rápidos en uno y una mejora media de unas 20 veces en el otro.
Path-walk ahora compatible con mapas de bits e islas delta
Cuando Git reempaqueta un repositorio, busca objetos similares que puedan almacenarse de forma eficiente como deltas. La búsqueda tradicional agrupa candidatos mediante un hash de nombre. El reempaquetado path-walk, en cambio, visita los objetos por su ubicación en el árbol, lo que acerca versiones de la misma ruta y a menudo encuentra mejores relaciones delta. El ahorro potencial de almacenamiento es sustancial. En una prueba con un clon reciente del repositorio Fluent UI y forzando a Git a recalcular deltas, un reempaquetado ordinario con mapas de bits produjo un paquete de 558,5 MB. El reempaquetado con --path-walk generó un paquete de 164,4 MB, aproximadamente un 71% menos.
Sin embargo, los servidores de repositorios necesitan algo más que paquetes pequeños. Suelen usar mapas de bits de alcanzabilidad para responder rápidamente a consultas de enumeración de objetos, y algunos emplean islas delta para evitar que los objetos de un grupo de referencias dependan de objetos disponibles solo en otro. El reempaquetado path-walk era incompatible con ambas cosas, lo que limitaba dónde podían desplegarse sus mejoras de almacenamiento. Git 2.56 elimina esas dos restricciones.
Un reempaquetado path-walk ahora puede seleccionar confirmaciones para un nuevo mapa de bits. Las invocaciones posteriores de git pack-objects pueden reutilizar un mapa de bits existente cuando responde a la petición, recurriendo al recorrido por rutas solo cuando sea necesario. El path-walk también realiza ahora la contabilidad requerida por las islas delta: propaga la pertenencia a islas a través de confirmaciones y árboles antes de elegir bases delta. Esto permite a los servidores preservar sus reglas de aislamiento mientras experimentan con la representación en disco más pequeña del path-walk. Estos cambios no habilitan el reempaquetado path-walk por defecto, sino que eliminan dos importantes bloqueos de adopción.
Git 2.56 introduce nuevas herramientas y mejoras internas
El comando experimental git history sigue creciendo. Git 2.54 lo introdujo con reword y split, y Git 2.55 añadió fixup. Ahora Git 2.56 incorpora git history drop, que elimina la confirmación seleccionada y reaplica sus descendientes sobre la confirmación padre. Si HEAD se mueve, Git actualiza el índice y el árbol de trabajo preservando cambios locales no relacionados. Se aborta si reaplicar un descendiente causara conflictos o sobrescribiera un cambio local. El comando sigue siendo experimental, no puede operar sobre una historia que contenga fusiones y no puede eliminar una confirmación raíz o de fusión.
Los comandos de escritura de referencias han estado dispersos entre git update-ref, git symbolic-ref y otras herramientas de plomería. Git 2.56 continúa consolidando la gestión de referencias de bajo nivel bajo el paraguas de git refs: create, update, delete y rename. Los valores antiguos opcionales proporcionan protección compare-and-swap para actualizaciones y borrados. Son comandos deliberadamente de bajo nivel; en particular, git refs rename mueve la referencia y su reflog, pero no realiza los ajustes de configuración de rama que hace git branch -m.
La limpieza de ramas temáticas locales después de que su trabajo llega a la rama principal puede implicar comparar cada rama con su rama de seguimiento remoto configurada. Git 2.56 añade una forma masiva: git branch --delete-merged 'origin/*' 'topic-*' --dry-run. En este comando, origin/* coincide con ramas cuyo upstream está bajo origin, topic-* limita los candidatos a nombres de rama locales que comienzan por topic-, y --dry-run lista lo que se borraría sin borrar nada. Cuando se ejecuta sin --dry-run, Git elimina solo las ramas cuyas puntas son alcanzables desde sus upstreams correspondientes. Las ramas extraídas en un árbol de trabajo, las ramas con upstreams faltantes y varias configuraciones de push ambiguas se omiten. La opción branch..deleteMerged = false puede proteger una rama de la limpieza masiva.
git bisect run es muy eficaz para encontrar la confirmación que introdujo una regresión, pero al terminar normalmente deja al usuario en la confirmación culpable hasta que ejecuta git bisect reset. La nueva opción --reset-when-found[=] combina esos pasos. Su valor por defecto es original, que vuelve a la confirmación en la que se estaba antes de comenzar la bisección; found limpia el estado de la bisección pero deja la confirmación culpable extraída. La opción no está disponible con --no-checkout.
El comando experimental git replay ahora puede aplanar la topología de fusiones con --linearize. Git reaplica las confirmaciones en una sola línea y descarta las confirmaciones de fusión, igualando la topología producida por git rebase --no-rebase-merges, pero sin usar el árbol de trabajo. La opción no se puede combinar con --contained ni con reaplicar múltiples ramas a la vez.
Git ahora puede reconocer dos errores tipográficos fáciles en la línea de comandos y sugerir la forma pretendida. Ejecutar git push origin/main aconseja usar git push origin main, mientras que git branch --set-upstream-to origin main sugiere git branch --set-upstream-to=origin/main. Git comprueba que cada corrección sea plausible antes de sugerirla, evitando conjeturas indiscriminadas.
Los clones parciales omiten objetos seleccionados inicialmente, pero los blobs obtenidos bajo demanda tradicionalmente se han quedado almacenados localmente para siempre. Git 2.56 puede descartar blobs grandes y recuperables y confiar de nuevo en el remoto promisor: git repack -a --filter=blob:limit=1m --drop-filtered --dry-run y luego sin --dry-run. El ensayo en seco lista los candidatos; el reempaquetado real los elimina localmente para que un acceso posterior los vuelva a buscar. Es un mecanismo de limpieza manual, no una caché acotada automáticamente, y actualmente solo admite filtros blob:limit. Git requiere un remoto promisor y rechaza casos inseguros como descartar un objeto referenciado por el índice actual. El trabajo fue contribuido como proyecto de GSoC por Siddharth Shrimali, con Christian Couder y Siddharth Asthana como mentores.
git log --follow ahora puede rastrear una ruta de forma más fiable a través de una historia no lineal. Anteriormente, el comando mantenía una única «ruta actual» global. Si diferentes padres renombraban la ruta de manera distinta, el lado que se visitara primero podía determinar lo que Git seguía durante el resto del recorrido. Git 2.56 registra la ruta por separado para cada padre, haciendo que el resultado sea independiente del orden de recorrido y corrigiendo casos como fusiones de subárboles.
Los gráficos con múltiples raíces pueden colocar una confirmación no relacionada directamente debajo de una confirmación raíz en la misma columna, haciendo que ambas parezcan conectadas. Git 2.56 indenta estas «raíces visuales» cuando es necesario. Esto está habilitado por defecto para git log --graph. Usa --no-graph-indent para el renderizado antiguo, o configura log.graphIndent para elegir un valor por defecto.
Varios cambios internos eliminan acantilados de escalado en operaciones por lo demás ordinarias. Las escrituras en reftable ahora evitan recargas redundantes después de tomar el bloqueo, haciendo que las llamadas de estadísticas del sistema de archivos sean constantes en lugar de lineales con el número de referencias. La carga de paquetes conocidos como nuevos ya no escanea la lista existente antes de cada inserción, eliminando una regresión O(N²) que hacía que un comando relacionado con el prompt tardara 4,5 segundos en un repositorio con 37.815 paquetes.
Otros cambios eliminaron escaneos cuadráticos de reftables con muchas tumbas y de diferencias de árbol de trabajo limitadas por ruta, mientras que la recolección de archivos no rastreados ahora es robustamente O(n log n) en lugar de depender de una entrada ya ordenada. Las pruebas de rendimiento de tumbas en reftable cayeron de unos 13 segundos a 0,2 segundos. En una copia de Chromium con aproximadamente 500.000 entradas de índice, una git diff afectada mejoró de unos ocho minutos a 0,07 segundos.
La versión 2.56 de Git se presenta como una actualización centrada en la seguridad y el rendimiento. Herramientas como git add --resolved reducen el riesgo de errores al preparar conflictos, mientras que las optimizaciones en la búsqueda de ancestros comunes y en el reempaquetado path-walk buscan aliviar el trabajo en repositorios de gran tamaño. A esto se suman mejoras en la gestión de referencias, limpieza de ramas y corrección de comandos, junto con notables avances en eficiencia interna que se traducen en tiempos de ejecución mucho menores. Todo ello configura un lanzamiento que, sin cambiar la experiencia básica del usuario, sienta bases más sólidas para el futuro del proyecto.
