El comando `git history`
(lalitm.com)- 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 fixupintegra 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 commitrewordcambia el mensaje de un commit anterior, ysplitdivide 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 dejjdesde 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 -iel á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 Gitgit historyes 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,rewordysplit
fixup: modificar un commit anterior y sus ramas derivadas al mismo tiempo
git history fixupcorrige 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 addy luego ejecutasgit history fixup <commit>para integrar esos cambios en el commit objetivo - Se parece a combinar
git commit --fixupcon un rebase autosquash, pero con la diferencia de que también actualiza otras ramas que contienen el commit objetivo - Mientras que
git rebase --update-refssolo mueve referencias dentro del rango actual del rebase,git historyreescribe 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 creaB*con un nuevo hash, y luegoCyDsobre ese commit también se recrean comoC*yD*- Los extremos de las ramas
feat-1yfeat-2también se mueven para seguir los nuevos commits
- Los extremos de las ramas
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 historyse 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 rewordcambia 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 rewordno 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 posteriorCse recrea comoC*yfeat-1pasa a apuntar al nuevo commit
split: dividir un commit en dos
git history splitpermite 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 congit rebase - Si divides
B, que contenía dos cambios no relacionados entre sí, se creanB1yB2, y el commit posteriorCse reconstruye comoC*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
git historyno reemplaza el modelo completo de trabajo que ofrecejj- En cambio,
git historysí permite introducir algunas ventajas dejjdentro de Git tradicional, al soportar corrección de commits anteriores, cambio de mensajes, división de commits y actualización de ramas derivadas dentro del flujo de trabajo actual - Como existe la posibilidad de relajar estas limitaciones en el futuro, todavía hay margen para mejoras adicionales en próximos lanzamientos de Git
1 comentarios
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.
git checkoutno 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 commit -am “Changes”por sí solo no basta.Existe
git rebase --abort, y también puedes crear una etiqueta antes del rebase y volver congit reset --hard $set_tag. Como el estado anterior no desaparece, no hay razón para temerle al rebase interactivo.tempobefore-rebasejusto para eso. Se usan como etiquetas, pero las ramas son más fáciles de mover, así que antes de unrebase -igrande normalmente ejecutogit branch before-rebase/some-feature.De hecho, casi nunca he necesitado
git reset before-rebase, pero sí he comparado muchas vecesgit log -p before-rebasecon la rama después del rebase para revisar cambios no intencionales en la resolución de conflictos.git reflog?jj undo.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.
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.
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 modificagit history. Aunque revisé el manual y el código fuente de Git, si modificas commits ya firmados con GPG usandogit history reword HEAD~, todas las firmas desaparecen.Como quería conservar la firma de los commits, al final volví al tradicional
git rebase -i.git historyes 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 originalAun 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...
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 extenderAun 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 Computerfixupya era posible congit commit --fixupo antes de eso congit rebase -i, yrewordtambién podía hacerse congit rebase -iParece 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
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 previaGit 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
jjes 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 internaPero es demasiado flexible, así que la interoperabilidad con Git depende mucho de convenciones y disciplina, y como casi cualquier estado de
jjpuede expresarse en Git, es fácil arruinar el repositorio remoto. Para recuperarlo, hay que entender Git a fondoSi 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 dejj, 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=48903054jjtiene 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. Unjjsin integración con Git todavía se parece más a un concepto que a algo realMe gusta
jjy 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 dejj, sería difícil encontrar una razón para instalarjjpor separadoLa forma en que
git history fixuphace rebase automático de todas las ramas descendientes no encaja con mi flujo de trabajo. Conservo cada versión de una rama de funcionalidad comomyfeature.1,myfeature.2, etc., para poder encontrar versiones anteriores cuando el comportamiento cambia en una versión nueva;git reflogno puede reemplazar esoPor ejemplo, si el problema apareció entre
myfeature.58ymyfeature.59y cada funcionalidad tiene 15 commits, congit range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59puedo 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ó bienGit reciente implementó tres tareas frecuentes de
git rebase --interactivecomo comandos separados con poca fricción, y parece que solo funcionan cuando no hay conflictosgit historysea realmente útil. Yo prefiero mover commits visualmente con rebase interactivo, y no querría señalar por error como objetivo degit historyel hash de un commit huérfano que ya desapareció de la rama local actualNo 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
"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
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-picko revertir commits de manera selectivaEntiendo que, si actualmente tengo
Den checkout y cambioBporB',git rebase --update-refsdejaEintacto por estar fuera del rango del rebase, y solo creaB' → C' → D', manteniendo elB → Eoriginal.En cambio, ¿
git history fixuptambién actualizaEhastaE'? Si es así, me da curiosidad si hay alguna forma de lograr con elgit rebasede toda la vida, que ya traigo en las manos desde hace décadas, una actualización de todas las ramas descendientesEigual. No he usado--update-refsdirectamente, pero si la documentación dice eso, entonces el resultado debería ser el tercer grafo, donde también apareceE'; o sea, el comportamiento que buscas ya es posible congit rebase