Git no está bien
(billjings.com)- Git tuvo éxito como repositorio distribuido de código fuente, pero su manejo del flujo de trabajo distribuido se parece más a una solución añadida después, y ahí se notan sus límites
- Los commits y ramas de Git no pueden expresar por sí mismos commits sucesores, historial de
amend, historial derebaseni estados descartados - En los Stacked PR, hay que encontrar los PR posteriores y hacer
rebasemanteniendo intacta la pila, pero a Git le cuesta identificar esa relación de forma confiable - Git deja estados mutables como staging, unstaged, sistema de archivos y HEAD fuera de los commits y las ramas, lo que vuelve más complejo aprenderlo y usarlo
- En flujos de desarrollo asíncrono donde varios PR deben usarse juntos antes del merge, el modelo de historial inmutable orientado hacia atrás de Git provoca problemas repetitivos
Los dos roles de Git
- Git se usa tanto como repositorio distribuido de código fuente como herramienta de flujo de trabajo distribuido
- Como repositorio de código ha sido un gran éxito, pero la forma en que maneja el flujo de trabajo distribuido se parece en gran parte a soluciones agregadas después
- El desarrollo asíncrono es, como lo expresa East River Source Control, casi una condición básica, y ocurre no solo cuando se colabora entre zonas horarias distintas, sino también al trabajar con uno mismo con diferencia de tiempo
- jj es una herramienta que deja más claras las limitaciones de Git, y es poco probable que quien sienta que Git es suficiente pruebe jj seriamente
Relaciones que el modelo básico de Git no capta
- En el centro de la forma de pensar de Git están los commits y las ramas
- Un commit es un objeto inmutable que contiene código fuente e historial
- Una rama es un puntero mutable con un registro adjunto
- Los diagramas típicos de Git dibujan commits como
C1,C2,C3, de modo que el orden y las relaciones parecen claras, pero en un repositorio real los nombres de los commits se parecen más a hashes o mensajes, así que esa relación de orden no existe dentro del sistema - Notaciones como
C2yC2’después de unrebaseson solo explicaciones fáciles de entender para humanos; Git no sabe que esos dos commits se corresponden entre sí - Para encontrar los commits sucesores de un commit específico, hay que recorrer todas las ramas y buscar los commits en la ruta que lleva desde ese commit, así que no es algo simple
En Git no existe la “C”
- Un commit de Git no puede saber por sí mismo la siguiente información
-
Commits sucesores
- El historial de modificaciones que conecta un commit viejo con uno nuevo después de un
amend
- El historial de modificaciones que conecta un commit viejo con uno nuevo después de un
-
Historial de
rebase- Si ese commit fue descartado o no
- Las ramas también tienen limitaciones
- Las ramas sí tienen una noción de historial, pero no es confiable asumir que correspondan 1:1 con cambios de código
- Las ramas no tienen relaciones entre sí y, por ejemplo, no se puede encontrar
wp/bugfixde manera confiable desdetrunk - Como no hay una referencia hacia adelante desde
trunkawp/bugfix, tampoco es una relación alcanzable - Los diagramas de Git parecen mostrar orden y correspondencia para un humano, pero pueden exagerar lo que la herramienta realmente ofrece
-
Por qué los Stacked PR son difíciles
- Si colaboras con personas en otras zonas horarias y no quieres hacer merge antes de la revisión, hay que pipelinear el trabajo como una CPU
- En vez de crear un PR y esperar a que termine la revisión, se crea un segundo PR encima del primero, luego otro encima de ese, y así varios PR secuenciales quedan en revisión al mismo tiempo: eso es un Stacked PR
- Git hace difícil manejar de forma confiable la estructura de Stacked PR
- Se puede crear un PR posterior como
Refactor key entry codeencima deFix key entry race, y luego, al hacerfetchdetrunky actualizar, hay que hacerrebasemanteniendo la pila - Como Git no conoce los commits sucesores, no es fácil ver
Refactor key entry codedesdeFix key entry race - El commit podría haber sido descartado, así que incluso si se puede ver un commit sucesor, es difícil saber si es el estado más reciente
- Las ramas se usan como si fueran el propio PR, pero en este flujo es fácil sobrescribirlas por error
- Se puede crear un PR posterior como
- Herramientas de stacking como Graphite pueden hacer esto sobre Git, pero no pueden reforzar los commits o las ramas de Git en sí
- Deben crear un repositorio separado de metadatos de ramas y sincronizarlo con Git
- Si el usuario manipula Git directamente, ese repositorio puede desalinearse con el estado de Git
El estado mutable está fuera de los commits
- Varios problemas de Git se derivan de que no modela directamente la mutabilidad
- En el flujo de edición de Git existe un estado aparte, fuera de los commits y las ramas
- El staging o index es un snapshot del código fuente creado desde la copia de trabajo, y de ahí se genera un nuevo commit
- Unstaged es un segundo diff que representa la diferencia entre el index y el sistema de archivos
- El sistema de archivos contiene el contenido checkoutado, al que se suman los cambios staged y unstaged
- HEAD es la ubicación donde se crea el nuevo commit
stashfunciona como un repositorio aparte que guarda y restaura el staging y los cambios unstaged- Si se cambia el checkout a otro commit o rama, Git intenta ajustar el sistema de archivos a la nueva posición mientras conserva los diffs de staging o unstaged
- Aunque los comandos sean distintos, si solo se observan las relaciones de flechas, este proceso tiene una forma parecida a un rebase que mueve el staging sobre una nueva base
Por qué es difícil modelar todo como commits
- El staging y la copia de trabajo también tienen ancestros claros y contienen código fuente, así que, si solo se ve el estado estático, podrían representarse como commits
- Pero como el ID de un commit es el hash de su contenido, si el commit fuera mutable, su ID estaría cambiando todo el tiempo
- Para apuntar de manera consistente a “qué son” el staging y la copia de trabajo, habría que tratarlos como ramas y no como commits, pero las ramas tienen las limitaciones ya vistas
- Esta complejidad lleva a problemas reales
- Aprender y usar Git se vuelve más difícil, porque el mismo concepto existe por separado en ambos lados
- El estado completo del repositorio difiere mucho del estado que se trae con un clone, así que exportarlo se vuelve incómodo
- Los flujos asíncronos donde el conjunto de cambios va variando con el tiempo no funcionan bien
- El sistema del lado mutable no puede expresar merges, así que a veces no logra representar el flujo de trabajo real
Casos donde Git no puede expresar el flujo de trabajo real
- Mientras desarrollas en una nueva rama de funcionalidad sin haber hecho commit todavía, puedes descubrir un bug en tu dispositivo que interfiere con el desarrollo
- Si ese bug no bloquea la nueva funcionalidad pero sí vuelve molesto el desarrollo, puedes hacer
stash, cambiarte a una rama nueva, crear una prueba de reproducción y una corrección, y luego abrir un PR - Después, al volver a la rama de la nueva funcionalidad, las opciones son limitadas
- Hacer
rebasedenew-featuresobrebugfix, aunque no haya una dependencia real, y seguir con la revisión - Durante el desarrollo, usar
new-featureconrebasesobrebugfix, y luego deshacer eserebaseantes de enviar la rama
- Hacer
- Con Git no se puede expresar el estado “el espacio de trabajo de edición debe contener a la vez todo el código de bugfix y el código de new feature que ya se commitió”
- Esta necesidad aparece con la misma estructura en problemas más difíciles, como pruebas de compatibilidad con PR que todavía no se han mergeado
- Con una herramienta adecuada, como Jujutsu megamerges, se pueden mantener varios PR en paralelo y aun así usarlos juntos en el espacio de edición
Git ya no es suficiente
- Las herramientas de control de versiones de inicios de los 2000 eran difíciles de usar y administrar, con calidad irregular, y estaba muy extendida la idea de que Subversion también era doloroso
- En ese momento no era común querer tener una copia completa del repositorio en local, ni tampoco era una demanda generalizada querer crear ramas locales
- A muchas personas les molestaba el file locking, pero otras pensaban que era necesario, e incluso preguntaban si en Git se podían bloquear archivos o directorios individuales
- Para quienes vivían directamente flujos de trabajo distribuidos, como en el software open source, los DVCS fueron recibidos como una venda que cubría heridas viejas
- Hoy, para quien usa un flujo de trabajo distribuido de forma significativa, el modelo de historial inmutable orientado hacia atrás de Git se convierte en una fuente repetitiva de problemas
- Empresas como Meta llevan casi 10 años usando sistemas internos muy por delante de Git
- La idea de que “ahora Claude manipula Git por ti” no vuelve irrelevantes esas alternativas
- Con el uso de LLM, parece que incluso dentro de una sola máquina los ingenieros están haciendo más desarrollo asíncrono que antes
1 comentarios
Opiniones en Lobste.rs
Habría estado bien que mostrara cómo jj resuelve los problemas planteados en el artículo
Para quienes usan jj quizá sea obvio, pero probablemente ellos no sean el público principal del texto
Personalmente, nunca he necesitado las funciones que el artículo pone como evidencia de que Git no está bien
Me pregunto si solo me pasa a mí
Uno de los puntos importantes de una herramienta es que forma parte de un sistema dinámico. Lo que la herramienta hace posible influye en “lo que creo que puedo hacer”, y esa creencia a su vez cambia la percepción de la herramienta y la dirección en la que evoluciona
Cuando una herramienta sacude el estado actual, también cambian las creencias y expectativas sobre lo que es posible hacer
Se ve interesante, pero el diagrama me marea
Sobre el comentario de que la situación actual no es tan grave como a inicios de los 2000, y que las limitaciones de los sistemas de control de versiones previos a Git eran bastante claras, Darcs salió antes que Git y en cierto modo corrigió de raíz algunos problemas del control de versiones basado en snapshots
Al inicio perdió terreno por su mal rendimiento, pero después mejoró, y la gente no volvió para revisarlo. Hay otros sistemas de control de versiones haciendo cosas interesantes, así que preferiría que no se presentara “si no es Git, entonces Jujutsu” como si fuera la única alternativa. Veo ese tipo de lógica demasiado seguido
Eso también es un problema del modelo de datos
¿Cómo maneja jj esto? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png
jj new A B, el commit de working copy puede tener varios padres, así que funciona como un commit de mergePor eso la working copy incorpora los cambios de ambos padres, y puedes seguir trabajando sobre ese merge o continuar haciendo amend a uno de los commits
Por ahora sigo prefiriendo Git, y el autor me parece sesgado
jj new, puedes mezclargityjjGit siempre apunta al commit padre, y el
jj commitactual pasa a verse como los cambios no confirmados del working treeYo aprendí
jjasí. Usabajjpara lo que hace bien, como manejar rebases o mover árboles, y seguía usando comandos degitpara tareas cotidianas en las que todavía no conocía el comando equivalente enjjo cuando pensaba primero en un comando de Git, comogit blameLa verdad, no terminé de entender por qué
jjera mejor hasta usarlo todos los días; solo leyendo sobre él pensaba “¿de verdad necesito esta función?” o “pero eso ya se puede hacer con Git”Claro,
jjtambién tiene desventajas. Si no tienes un.gitignoreactualizado, un archivo binario puede terminar en un commit por accidente. Por suertejjavisa si intentas agregar un archivo muy grande, pero los pequeños pueden colarseSi durante una depuración tienes archivos rastreados o logs en el directorio actual, también pueden entrar, así que conviene revisar todo el diffstat después de manipular el árbol
En particular, puede ser un problema si haces búsqueda binaria con
jjy terminas probando un commit anterior a aquel donde se actualizó.gitignore. Tal vez la búsqueda binaria debería tener un modo de solo lectura