1 puntos por GN⁺ 2 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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 de rebase ni estados descartados
  • En los Stacked PR, hay que encontrar los PR posteriores y hacer rebase manteniendo 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 C2 y C2’ después de un rebase son 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
    • 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/bugfix de manera confiable desde trunk
      • Como no hay una referencia hacia adelante desde trunk a wp/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 code encima de Fix key entry race, y luego, al hacer fetch de trunk y actualizar, hay que hacer rebase manteniendo la pila
    • Como Git no conoce los commits sucesores, no es fácil ver Refactor key entry code desde Fix 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
  • 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
  • stash funciona 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 rebase de new-feature sobre bugfix, aunque no haya una dependencia real, y seguir con la revisión
    • Durante el desarrollo, usar new-feature con rebase sobre bugfix, y luego deshacer ese rebase antes de enviar la rama
  • 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

 
GN⁺ 2 시간 전
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í

    • No eres el único en absoluto
      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

    • Me da un poco de risa que el autor diga que el modelo de datos de Git es muy bueno, pero luego mencione de pasada que el flujo de trabajo no es bueno porque las ramas de Git son solo punteros al final de una rama
      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

    • Si usas jj new A B, el commit de working copy puede tener varios padres, así que funciona como un commit de merge
      Por 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

    • Me da curiosidad si prefieres Git porque no has pasado por las situaciones que el autor dice que son difíciles en Git y ya estás acostumbrado a Git, o si incluso pasando por esas situaciones piensas que Git ofrece un mejor flujo de trabajo que Jujutsu
    • Si solo recuerdas que tienes que ejecutar jj new, puedes mezclar git y jj
      Git siempre apunta al commit padre, y el jj commit actual pasa a verse como los cambios no confirmados del working tree
      Yo aprendí jj así. Usaba jj para lo que hace bien, como manejar rebases o mover árboles, y seguía usando comandos de git para tareas cotidianas en las que todavía no conocía el comando equivalente en jj o cuando pensaba primero en un comando de Git, como git blame
      La verdad, no terminé de entender por qué jj era 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, jj también tiene desventajas. Si no tienes un .gitignore actualizado, un archivo binario puede terminar en un commit por accidente. Por suerte jj avisa si intentas agregar un archivo muy grande, pero los pequeños pueden colarse
      Si 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 jj y 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