1 puntos por GN⁺ 2 시간 전 | 1 comentarios | Compartir por WhatsApp
  • El comando experimental git history, introducido entre Git 2.54 y 2.55, automatiza la modificación de commits pasados y la reorganización de ramas derivadas sin cambiar el flujo de trabajo tradicional de Git
  • fixup integra los cambios en staging dentro de un commit anterior y luego hace rebase automáticamente de todas las ramas locales derivadas de ese commit para ajustarlas al nuevo hash del commit
  • reword cambia el mensaje de un commit anterior, y split divide los cambios de un commit en dos commits separados por hunks, luego reorganiza los commits y ramas posteriores
  • Los tres subcomandos usan procesamiento atómico que rechaza operaciones con posibilidad de conflicto, evitando que el árbol de trabajo quede roto a medias, pero no funcionan en historiales con commits de merge
  • Aunque no ofrece el manejo de conflictos como entidad de primera clase de jj, ni deshacer basado en registro de operaciones, ni el modelado de la copia de trabajo como commit, sí permite usar varias funciones de edición de historial al estilo de jj desde la distribución principal de Git sin instalar nada aparte

Por qué editar el historial en Git tradicional es incómodo

  • Cuando manejas varios cambios en paralelo, tienes que estar saltando entre ramas y commits, y durante rebase -i el árbol de trabajo puede quedar en un estado incompleto
  • jj, una alternativa a Git, suele mencionarse como solución a estos problemas, pero incluso probándolo durante varios días cada tres meses a lo largo de año y medio, al final uno termina volviendo al flujo de trabajo tradicional de Git
  • git history es un comando experimental incluido en la distribución principal de Git, así que puede probarse sin instalar herramientas adicionales
  • Los subcomandos se componen de tres opciones: fixup, reword y split

fixup: modificar un commit anterior y sus ramas derivadas al mismo tiempo

  • git history fixup corrige una parte errónea de un commit anterior y hace rebase automáticamente de las ramas derivadas de ese commit
  • Igual que en un flujo de corrección normal, haces staging de los cambios con git add y luego ejecutas git history fixup <commit> para integrar esos cambios en el commit objetivo
  • Se parece a combinar git commit --fixup con un rebase autosquash, pero con la diferencia de que también actualiza otras ramas que contienen el commit objetivo
  • Mientras que git rebase --update-refs solo mueve referencias dentro del rango actual del rebase, git history reescribe todas las ramas locales derivadas del commit objetivo
    • Si hace falta, puede limitarse el alcance de la actualización solo a la rama actual
    • No funciona en historiales con commits de merge, así que puede ser difícil aplicarlo en algunos estilos de uso de Git
  • Si integras cambios en B, se crea B* con un nuevo hash, y luego C y D sobre ese commit también se recrean como C* y D*
    • Los extremos de las ramas feat-1 y feat-2 también se mueven para seguir los nuevos commits

Procesamiento atómico y límites frente a conflictos

  • Los tres subcomandos se ejecutan de forma atómica, así que no dejan el árbol de trabajo en un estado roto a medias
  • La atomicidad se garantiza rechazando directamente operaciones que podrían provocar conflictos
  • Eso los hace más limitados, en términos funcionales, que jj, que trata los conflictos como objetos de primera clase y permite conservar el estado de conflicto durante un rebase para resolverlo más tarde
  • Las limitaciones de git history se aplican deliberadamente para evitar que la reescritura del historial se convierta en una operación de preservación de estado
    • Si Git llega a admitir conflictos de primera clase en el futuro, esas limitaciones podrían levantarse

reword: cambiar mensajes de commit sin tocar el árbol de trabajo

  • git history reword cambia el mensaje de un commit anterior y reconstruye automáticamente los commits y ramas que vienen después
  • Al ejecutar git history reword <commit>, se abre el mensaje actual en el editor; después de modificarlo y guardarlo, la pila de commits posteriores se reconstruye con los nuevos hashes
  • Es útil cuando el diseño cambia durante el desarrollo iterativo y necesitas corregir mensajes anteriores para que reflejen la estructura actual
  • Funciona de la misma manera que fixup, salvo que cambia solo el mensaje del commit y no el contenido del árbol
  • reword no toca ni el índice ni el árbol de trabajo: solo modifica el grafo de commits
    • Incluso puedes editar commits de ramas que no tienes checkout sin interrumpir tu trabajo actual
    • Si cambias el mensaje de B, su hash cambia, por lo que el commit posterior C se recrea como C* y feat-1 pasa a apuntar al nuevo commit

split: dividir un commit en dos

  • git history split permite seleccionar interactivamente los cambios de un commit para dividirlo en dos commits
  • Al ejecutar git history split <commit>, se abre un prompt para elegir los cambios del commit objetivo por hunk
    • Los hunks seleccionados para conservar forman el primer commit
    • Los hunks restantes pasan al segundo commit
  • Se parece a git add -p, pero sin necesitar el procedimiento complejo de combinarlo con git rebase
  • Si divides B, que contenía dos cambios no relacionados entre sí, se crean B1 y B2, y el commit posterior C se reconstruye como C* sobre esos dos commits
  • Es el más especializado de los tres comandos, pero resulta útil cuando realmente necesitas partir un commit

Diferencias funcionales frente a jj

1 comentarios

 
GN⁺ 2 시간 전
Comentarios en Hacker News
  • Git me resultaba incómodo, pero mejoró después de leer los primeros tres capítulos del libro gratuito Pro Git. Ofrece un excelente modelo mental sobre el funcionamiento interno de Git y, para bien o para mal, la interfaz de usuario de Git refleja directamente su estructura interna, así que al entender eso todo empezó a encajar.

    • No estoy de acuerdo con que la interfaz de usuario refleje directamente la estructura interna. Por ejemplo, git checkout no pierde los cambios que aún no han entrado a Git cuando cambias de rama, pero cuando restauras otra versión de un archivo está diseñado para revertir cambios; ¿cómo refleja eso directamente la estructura interna?
      Aparte de eso, la inconsistencia del CLI también hace que Git sea difícil de usar, porque incluso si sabes qué operación y comando quieres internamente, sigue siendo difícil encontrar la sintaxis exacta de la línea de comandos.
    • Git no es tan confuso como se dice; solo hace falta dedicarle unas cuantas horas para entenderlo. Es una herramienta esencial para cualquier ingeniero de software, y es una pena no aprenderla; si eres profesional, git commit -am “Changes” por sí solo no basta.
    • A mí también me confundían los comandos de Git, pero después de ver un video en YouTube donde Linus explica las estructuras de datos internas de Git, entendí que era muy simple. Pude relacionar fácilmente cómo las funciones de la herramienta manipulan las estructuras de datos y, de hecho, son las abstracciones de más alto nivel que Git intenta ofrecer las que vuelven todo más confuso.
    • No es así. La mitad de los comandos de Git son de bajo nivel y la otra mitad de alto nivel, así que no tienen una correspondencia 1:1 con la estructura interna.
    • La experiencia de usuario de Mercurial era, por mucho, la mejor, y se podía aprender sin leer documentación aparte. Incluso cuando uso Git, lo he usado como si fuera Mercurial y, cuando algo sale mal, simplemente borro el repositorio local; así he sobrevivido toda mi carrera.
  • Existe git rebase --abort, y también puedes crear una etiqueta antes del rebase y volver con git reset --hard $set_tag. Como el estado anterior no desaparece, no hay razón para temerle al rebase interactivo.

    • Mucha gente crea ramas llamadas temp o before-rebase justo para eso. Se usan como etiquetas, pero las ramas son más fáciles de mover, así que antes de un rebase -i grande normalmente ejecuto git branch before-rebase/some-feature.
      De hecho, casi nunca he necesitado git reset before-rebase, pero sí he comparado muchas veces git log -p before-rebase con la rama después del rebase para revisar cambios no intencionales en la resolución de conflictos.
    • Ni siquiera hace falta crear una etiqueta de seguridad aparte; reflog cumple ese papel automáticamente.
    • ¿Usas git reflog?
    • Nada puede ser más simple que jj undo.
    • Exacto. La mitad de las veces que escribo git rebase, enseguida termino escribiendo --abort.
  • No me considero coder ni programador, pero aprender Git me dio una especie de superpoder de organización en mi forma de pensar. Lo uso para muchísimas cosas: sitios web, proyectos de diseño, electrónica, composición musical, almacenamiento personal de conocimiento, scripts de administración remota, gestión de configuraciones, fragmentos de código y más; no es perfecto, pero igual recomiendo que todos los que trabajan conmigo lo aprendan.

    • A mí también me mejoró mucho la capacidad de organización, aunque eso tiene más que ver con los sistemas de control de versiones en general que con Git en sí. Un sistema de control de versiones te permite organizar y conservar lo que haces, y sacar copias de lo que quieras en cualquier momento, para trabajar sin preocuparte.
      En Getting Things Done de David Allen se habla de la necesidad de tener un sistema confiable para recopilar y organizar pensamientos. Yo uso OmniFocus para tareas, proyectos y listas, pero para organizar y preservar el software y algunos datos que creo en la computadora, confío en Git.
    • ¿Cómo usas Git para composición musical?
  • En teoría me encantan estos comandos y siempre uso los equivalentes en jj, pero no he podido encontrar cómo firmar los commits que modifica git history. Aunque revisé el manual y el código fuente de Git, si modificas commits ya firmados con GPG usando git history reword HEAD~, todas las firmas desaparecen.
    Como quería conservar la firma de los commits, al final volví al tradicional git rebase -i.

    • Eso parece una omisión importante y da la impresión de que evita el flujo estándar de manejo de commits. Como el manual también dice que git history es una función experimental, parece algo que vale la pena reportar como bug.
  • Incluso sin comandos nuevos, he venido modificando de forma importante el historial local de commits de Git desde hace más de 10 años sin grandes dificultades. Estoy convencido de que una gran razón por la que Git y rebase se sienten difíciles es un problema de interfaz de usuario: hay que averiguar por cuenta propia las opciones exactas de la línea de comandos y cómo funcionan
    En Windows, el cuadro de diálogo de rebase de TortoiseGit era potente y a la vez fácil de usar. Bastaba con hacer clic derecho sobre el commit base en el registro de commits y elegir "Rebase onto this"; además, como en https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm..., reordenar, dividir, combinar y editar commits era claro, y si cancelabas a mitad del proceso todo volvía a su estado original
    Aun así, como usa extensiones del shell, hay demasiados clics; sería mejor si fuera una app independiente con pestañas, pero eso parece ser un principio de diseño heredado de TortoiseSVN. También probé Git Extensions por un tiempo, pero no encontré funciones que usaba con frecuencia o ya estaba acostumbrado a TortoiseGit, así que no terminé adoptándolo. La pantalla de registro puede verse en https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...

    • Una de las cosas que extrañé al pasar de Windows a Linux fue el shell integrado donde cualquier programa podía agregar elementos al menú contextual de archivos y carpetas, overlays como los iconos de estado de TortoiseGit y objetos especiales como My Computer. Entiendo que eso llegó a su punto máximo en XP y que desde Vista Microsoft lo cambió en una dirección más difícil de extender
      Aun así, todavía no entiendo por qué cierto menú de VC++6 creaba un objeto del shell con apariencia de carpeta en lugar de una lista normal y lo mostraba en el cuadro de diálogo para abrir archivos. Era el asistente para agregar componentes al proyecto, y mientras ese cuadro estaba abierto yo solía buscar esa carpeta falsa dentro de My Computer
    • Es cierto que el flujo de trabajo anterior era un poco más engorroso y que los comandos nuevos son más convenientes. fixup ya era posible con git commit --fixup o antes de eso con git rebase -i, y reword también podía hacerse con git rebase -i
      Parece que se acumuló suficiente retroalimentación de que resultaba incómodo editar un archivo para decidir qué acción ejecutar en cada commit, y por eso agregaron comandos de más alto nivel que envuelven la funcionalidad existente
    • Entre las interfaces de usuario para Git, nunca he visto una herramienta que se acerque a Magit. Ofrece funciones profundas junto con una interfaz simplificada y con estado
      Las herramientas de línea de comandos hacen difícil acceder a la salida anterior una vez que ejecutas el siguiente comando. Aunque quieras hacer cherry-pick de un commit visto en git log, si el hash ya no quedó visible en la terminal no puedes escribirlo solo de memoria, pero una interfaz con estado permite mantener varias interacciones en paralelo y conservar la información previa
      Git tiene muchísima información de estado que hay que recordar: la rama actual, la rama anterior, los archivos modificados, los commits no enviados, etc. Si no aprendes herramientas alrededor de eso, como indicadores de rama y autocompletado del shell, varias pestañas o divisiones de ventana, copiar entre pestañas y búsqueda con autocompletado, usar Git puede volverse muy desagradable
  • Considero que la debilidad real de jj es solo la integración con Git. Formaliza lenguajes específicos de dominio para conjuntos de archivos, conjuntos de revisiones y plantillas, los usa de forma consistente en toda la configuración y la CLI, y ofrece conceptos potentes como interfaz sin exponer directamente la estructura interna
    Pero es demasiado flexible, así que la interoperabilidad con Git depende mucho de convenciones y disciplina, y como casi cualquier estado de jj puede expresarse en Git, es fácil arruinar el repositorio remoto. Para recuperarlo, hay que entender Git a fondo
    Si fuera un proyecto nuevo de desarrollo en solitario, valdría la pena probar jj. Viéndolo en condiciones favorables, se siente como Git en sus inicios frente a Subversion, y si primero te acostumbras al modo de operar de jj, después estarás un poco mejor preparado para la dura integración con Git. Hay contexto relacionado en https://news.ycombinator.com/item?id=48903054

    • En principio, jj tiene backends que no son Git, pero por ahora el único que se indica explícitamente como adecuado para un entorno operativo es el backend de Git. Un jj sin integración con Git todavía se parece más a un concepto que a algo real
      Me gusta jj y su modelo mental de control de versiones, así que seguiré usándolo, pero a veces tengo que bajar a Git para terminar el trabajo, por lo que se siente más como un wrapper conveniente sobre Git que como un sistema de gestión de configuración nuevo. Si Git agregara comandos que respalden el flujo de trabajo de jj, sería difícil encontrar una razón para instalar jj por separado
  • La forma en que git history fixup hace rebase automático de todas las ramas descendientes no encaja con mi flujo de trabajo. Conservo cada versión de una rama de funcionalidad como myfeature.1, myfeature.2, etc., para poder encontrar versiones anteriores cuando el comportamiento cambia en una versión nueva; git reflog no puede reemplazar eso
    Por ejemplo, si el problema apareció entre myfeature.58 y myfeature.59 y cada funcionalidad tiene 15 commits, con git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59 puedo ver, commit por commit, la diferencia entre ambas versiones aunque sus commits base sean distintos. Por eso no quiero un rebase automático de todas las ramas que incluyan cierto commit, y usar etiquetas para las versiones antiguas tampoco me funcionó bien

  • Git reciente implementó tres tareas frecuentes de git rebase --interactive como comandos separados con poca fricción, y parece que solo funcionan cuando no hay conflictos

    • Dudo que git history sea realmente útil. Yo prefiero mover commits visualmente con rebase interactivo, y no querría señalar por error como objetivo de git history el hash de un commit huérfano que ya desapareció de la rama local actual
  • No entiendo por qué se esfuerzan tanto en pulir perfectamente el historial de Git. Total, nadie vuelve a leer los commits individuales, así que antes de hacer merge basta con squashearlos todos

    • Totalmente en desacuerdo. Cuando reviso un PR, al menos leo el historial de commits para entender el flujo de cambios, y si está lleno de "oops" y "fix", lo rechazo de inmediato.
      Los commits cuentan la historia del trabajo, así que hay que ofrecer un historial de commits fácil de leer para que quien revisa no tenga que esforzarse más en entender la intención
    • Yo sí leo los commits individuales con frecuencia. En repositorios bien mantenidos, son muy útiles para rastrear bugs o regresiones y entender la intención detrás del cambio del autor
    • Cuando me pregunto por qué el código terminó así, ayuda mucho ver el contexto del commit que introdujo ese código. Si es un commit gigante que mete cambios distintos todos juntos, su valor cae muchísimo
    • Esa forma de pensar delata que no trabajas en un equipo grande. Cuando vuelvo de vacaciones, lo primero que hago es leer el historial de Git para ver qué cambió mientras no estaba, y también sirve cuando se rompe el CI, así que en la práctica reviso el historial de alguna forma casi todos los días.
      Si el codebase y las funcionalidades son muy pequeñas, quizá sí convenga hacer squash de todo, pero yo prefiero una forma de trabajo en la que pueda ver el flujo real de cambios y hacer cherry-pick o revertir commits de manera selectiva
    • Un historial bien ordenado, con un commit por cada cambio lógico, es tremendamente útil al hacer búsqueda binaria de regresiones
  • Entiendo que, si actualmente tengo D en checkout y cambio B por B', git rebase --update-refs deja E intacto por estar fuera del rango del rebase, y solo crea B' → C' → D', manteniendo el B → E original.
    En cambio, ¿git history fixup también actualiza E hasta E'? Si es así, me da curiosidad si hay alguna forma de lograr con el git rebase de toda la vida, que ya traigo en las manos desde hace décadas, una actualización de todas las ramas descendientes

    • El hash de un commit también refleja el hash de su commit padre, así que no se puede dejar E igual. No he usado --update-refs directamente, pero si la documentación dice eso, entonces el resultado debería ser el tercer grafo, donde también aparece E'; o sea, el comportamiento que buscas ya es posible con git rebase
    • Puedes consultar https://blog.hot-coffee.dev/en/blog/git_update_refs/ para más detalles