Como desarrollador, trabajas con Git todo el tiempo.
¿Alguna vez llegaste a un punto en el que dijiste: "Uh-oh, qué acabo de hacer?"
Esta publicación le dará las herramientas para reescribir la historia con confianza.
También di una charla en vivo cubriendo el contenido de esta publicación. Si prefiere un video (o desea verlo junto con la lectura), puede encontrarlo
¡Estoy trabajando en un libro sobre Git! ¿Está interesado en leer las versiones iniciales y proporcionar comentarios? Envíame un correo electrónico: [email protected]
Antes de comprender cómo deshacer cosas en Git, primero debe comprender cómo registramos los cambios en Git. Si ya conoce todos los términos, no dude en omitir esta parte.
Es muy útil pensar en Git como un sistema para registrar instantáneas de un sistema de archivos en el tiempo. Considerando un repositorio Git, tiene tres “estados” o “árboles”:
Por lo general, cuando trabajamos en nuestro código fuente, lo hacemos desde un directorio de trabajo . Un directorio de trabajo (ectrory) (o árbol de trabajo ) es cualquier directorio en nuestro sistema de archivos que tiene un repositorio asociado.
Contiene las carpetas y archivos de nuestro proyecto y también un directorio llamado .git
. Describí el contenido de la carpeta .git
con más detalle en
Después de realizar algunos cambios, es posible que desee registrarlos en su repositorio . Un repositorio (en resumen: repo ) es una colección de confirmaciones , cada una de las cuales es un archivo de cómo se veía el árbol de trabajo del proyecto en una fecha pasada, ya sea en su máquina o en la de otra persona.
Un repositorio también incluye otras cosas además de nuestros archivos de código, como HEAD
, ramas, etc.
En el medio, tenemos el índice o área de ensayo ; estos dos términos son intercambiables. Cuando checkout
una rama, Git completa el índice con todo el contenido del archivo que se desprotegió por última vez en nuestro directorio de trabajo y cómo se veía cuando se desprotegió originalmente.
Cuando usamos git commit
, el commit se crea basado en el estado del índice .
Entonces, el índice, o el área de preparación, es su campo de juego para la próxima confirmación. Puede trabajar y hacer lo que quiera con el índice , agregarle archivos, eliminar cosas de él y luego, solo cuando esté listo, continuar y comprometerse con el repositorio.
Es hora de ponerse manos a la obra 🙌🏻
Use git init
para inicializar un nuevo repositorio. Escriba algo de texto en un archivo llamado 1.txt
:
De los tres estados de árbol descritos anteriormente, ¿dónde está 1.txt
ahora?
En el árbol de trabajo, ya que aún no se ha introducido en el índice.
Para organizarlo , para agregarlo al índice, use git add 1.txt
.
Ahora, podemos usar git commit
para confirmar nuestros cambios en el repositorio.
Ha creado un nuevo objeto de confirmación, que incluye un puntero a un árbol que describe todo el árbol de trabajo. En este caso, será solo 1.txt
dentro de la carpeta raíz. Además de un puntero al árbol, el objeto de confirmación incluye metadatos, como marcas de tiempo e información del autor.
Para obtener más información sobre los objetos en Git (como confirmaciones y árboles),
(Sí, "echa un vistazo", juego de palabras 😇)
Git también nos dice el valor SHA-1 de este objeto de confirmación. En mi caso, fue c49f4ba
(que son solo los primeros 7 caracteres del valor SHA-1, para ahorrar espacio).
Si ejecuta este comando en su máquina, obtendrá un valor SHA-1 diferente, ya que es un autor diferente; Además, crearía la confirmación en una marca de tiempo diferente.
Cuando inicializamos el repositorio, Git crea una nueva rama (llamada main
por defecto). Ymain
. ¿Qué pasa si tienes varias sucursales? ¿Cómo sabe Git qué rama es la rama activa?
Git tiene otro puntero llamado HEAD
, que apunta (generalmente) a una rama, que luego apunta a una confirmación. Por cierto,HEAD
¡Es hora de introducir más cambios en el repositorio!
Ahora, quiero crear otro. Entonces, creemos un nuevo archivo y agréguelo al índice, como antes:
Ahora es el momento de usar git commit
. Es importante destacar git commit
hace dos cosas:
Primero, crea un objeto de confirmación, por lo que hay un objeto dentro de la base de datos interna de objetos de Git con un valor SHA-1 correspondiente. Este nuevo objeto de confirmación también apunta a la confirmación principal. Esa es la confirmación a la que apuntaba HEAD
cuando escribiste el comando git commit
.
En segundo lugar, git commit
mueve el puntero de la rama activa; en nuestro caso, eso sería main
, para apuntar al objeto de confirmación recién creado.
Para reescribir el historial, comencemos por deshacer el proceso de introducir una confirmación. Para eso, conoceremos el comando git reset
, una herramienta súper poderosa.
git reset --soft
Entonces, el último paso que hiciste antes fue git commit
, lo que en realidad significa dos cosas: Git creó un objeto de confirmación y movió main
, la rama activa. Para deshacer este paso, usa el comando git reset --soft HEAD~1
.
La sintaxis HEAD~1
se refiere al primer padre de HEAD
. Si tuviera más de una confirmación en el gráfico de confirmación, diga "Commit 3" apuntando a "Commit 2", que, a su vez, apunta a "Commit 1".
Y digamos que HEAD
estaba apuntando a "Commit 3". Podría usar HEAD~1
para referirse a "Commit 2", y HEAD~2
se referiría a "Commit 1".
Entonces, volvamos al comando: git reset --soft HEAD~1
Este comando le pide a Git que cambie lo que apunta HEAD
. (Nota: en los diagramas a continuación, uso *HEAD
para "lo que sea que HEAD
esté señalando"). En nuestro ejemplo, HEAD
apunta a main
. Entonces Git solo cambiará el puntero de main
para que apunte a HEAD~1
. Es decir, main
apuntará a "Commit 1".
Sin embargo, este comando no afectó el estado del índice o el árbol de trabajo. Entonces, si usa git status
verá que 2.txt
está preparado, al igual que antes de ejecutar git commit
.
¿Qué pasa con git log?
Comenzará desde HEAD
, irá a main
y luego a "Commit 1". Tenga en cuenta que esto significa que "Commit 2" ya no es accesible desde nuestro historial.
¿Significa eso que se elimina el objeto de confirmación de "Commit 2"? 🤔
No, no se elimina. Todavía reside dentro de la base de datos interna de objetos de Git.
Si envía el historial actual ahora, usando git push
, Git no enviará "Commit 2" al servidor remoto, pero el objeto de confirmación todavía existe en su copia local del repositorio.
Ahora, confirme nuevamente y use el mensaje de confirmación de "Commit 2.1" para diferenciar este nuevo objeto del "Commit 2" original:
¿Por qué son diferentes "Commit 2" y "Commit 2.1"? Incluso si usamos el mismo mensaje de confirmación, y aunque apunten a1.txt
y 2.txt
), todavía tienen marcas de tiempo diferentes, ya que se crearon en momentos diferentes.
En el dibujo de arriba, mantuve "Commit 2" para recordarte que todavía existe en la base de datos interna de objetos de Git. Tanto "Commit 2" como "Commit 2.1" ahora apuntan a "Commit 1", pero solo se puede acceder a "Commit 2.1" desde HEAD
.
Es hora de ir incluso hacia atrás y deshacer más. Esta vez, use git reset --mixed HEAD~1
(nota: --mixed
es el interruptor predeterminado para git reset
).
Este comando comienza igual que git reset --soft HEAD~1
. Lo que significa que toma el puntero de lo que HEAD
esté apuntando ahora, que es la rama main
, y lo establece en HEAD~1
, en nuestro ejemplo, "Commit 1".
Luego, Git va más allá y deshace efectivamente los cambios que hicimos en el índice. Es decir, cambiando el índice para que coincida con el HEAD
actual, el HEAD
nuevo después de configurarlo en el primer paso.
Si ejecutamos git reset --mixed HEAD~1
, significa que HEAD
se establecería en HEAD~1
("Commit 1"), y luego Git haría coincidir el índice con el estado de "Commit 1"; en este caso, significa que 2.txt
ya no formará parte del índice.
Es hora de crear un nuevo compromiso con el estado del "Commit 2" original. Esta vez necesitamos volver a preparar 2.txt
antes de crearlo:
¡Adelante, deshazte aún más!
Continúe y ejecute git reset --hard HEAD~1
Nuevamente, Git comienza con la etapa --soft
, configurando cualquier HEAD
al que apunte ( main
), a HEAD~1
("Commit 1").
Hasta ahora, todo bien.
A continuación, pasar a la etapa --mixed
, haciendo coincidir el índice con HEAD
. Es decir, Git deshace la preparación de 2.txt
.
Es hora del paso --hard
donde Git va más allá y hace coincidir el directorio de trabajo con la etapa del índice. En este caso, significa eliminar 2.txt
también del directorio de trabajo.
(**Nota: en este caso específico, el archivo no se rastrea, por lo que no se eliminará del sistema de archivos; sin embargo, no es realmente importante para comprender git reset
).
Entonces, para introducir un cambio en Git, tienes tres pasos. Cambia el directorio de trabajo, el índice o el área de preparación y luego confirma una nueva instantánea con esos cambios. Para deshacer estos cambios:
git reset --soft
, deshacemos el paso de confirmación.
git reset --mixed
, también deshacemos el paso de preparación.
git reset --hard
, deshacemos los cambios en el directorio de trabajo. Entonces, en un escenario de la vida real, escriba "Me encanta Git" en un archivo ( love.txt
), ya que todos amamos a Git 😍. Adelante, prepara y comete esto también:
¡Uy!
En realidad, no quería que lo cometiera.
Lo que realmente quería que hicieras es escribir algunas palabras de amor más en este archivo antes de enviarlo.
¿Qué puedes hacer?
Bueno, una forma de superar esto sería usar git reset --mixed HEAD~1
, deshaciendo efectivamente las acciones de confirmación y puesta en escena que tomó:
Entonces, los puntos main
para "Commit 1" nuevamente, y love.txt
ya no es parte del índice. Sin embargo, el archivo permanece en el directorio de trabajo. Ahora puede continuar y agregarle más contenido:
Continúe, organice y confirme su archivo:
Bien hecho 👏🏻
Obtuviste esta historia clara y agradable de "Commit 2.4" apuntando a "Commit 1".
Ahora tenemos una nueva herramienta en nuestra caja de herramientas, git reset
💪🏻
Esta herramienta es súper, súper útil y puedes lograr casi cualquier cosa con ella. No siempre es la herramienta más conveniente para usar, pero es capaz de resolver casi cualquier escenario de reescritura de la historia si la usa con cuidado.
Para los principiantes, recomiendo usar solo git reset
durante casi cualquier momento que desee deshacer en Git. Una vez que se sienta cómodo con él, es hora de pasar a otras herramientas.
Consideremos otro caso.
Cree un nuevo archivo llamado new.txt
; etapa y cometer:
Ups. En realidad, eso es un error. Estabas en main
y quería que crearas esta confirmación en una rama de características. Mi mal 😇
Hay dos herramientas más importantes que quiero que tome de esta publicación. El segundo es git reset
. El primero y mucho más importante es comparar el estado actual con el estado en el que desea estar.
Para este escenario, el estado actual y el estado deseado se ven así:
Notarás tres cambios:
puntos main
a "Commit 3" (el azul) en el estado actual, pero a "Commit 2.4" en el estado deseado.
La rama feature
no existe en el estado actual, pero existe y apunta a "Commit 3" en el estado deseado.
HEAD
apunta a main
en el estado actual y a feature
en el estado deseado.
Si puede dibujar esto y sabe cómo usar git reset
, definitivamente puede salir de esta situación.
De nuevo, lo más importante es respirar y sacar esto.
Observando el dibujo de arriba, ¿cómo pasamos del estado actual al deseado?
Por supuesto, hay algunas formas diferentes, pero presentaré una opción solo para cada escenario. Siéntase libre de jugar con otras opciones también.
Puede comenzar usando git reset --soft HEAD~1
. Esto establecería main
para apuntar a la confirmación anterior, "Commit 2.4":
Mirando de nuevo el diagrama actual-vs-deseado, puede ver que necesita una nueva rama, ¿verdad? Puede usar git switch -c feature
para ello o git checkout -b feature
(que hace lo mismo):
Este comando también actualiza HEAD
para apuntar a la nueva rama.
Dado que usó git reset --soft
, no cambió el índice, por lo que actualmente tiene exactamente el estado que desea confirmar, ¡qué conveniente! Simplemente puede comprometerse con la rama feature
:
Y llegaste al estado deseado 🎉
¿Listo para aplicar su conocimiento a casos adicionales?
Agregue algunos cambios a love.txt
y también cree un nuevo archivo llamado cool.txt
. Ponlos en escena y comprométete:
Vaya, en realidad quería que crearas dos confirmaciones separadas, una con cada cambio 🤦🏻
¿Quieres probar este tú mismo?
Puede deshacer los pasos de compromiso y preparación:
Siguiendo este comando, el índice ya no incluye esos dos cambios, pero ambos todavía están en su sistema de archivos. Así que ahora, si solo preparas love.txt
, puedes enviarlo por separado y luego hacer lo mismo con cool.txt
:
Bonito 😎
Cree un nuevo archivo ( new_file.txt
) con algo de texto y agregue algo de texto a love.txt
. Preparar ambos cambios y confirmarlos:
Uy 🙈🙈
Entonces, esta vez, quería que fuera en otra rama, pero no en una rama nueva , sino en una rama ya existente.
¿Entonces que puedes hacer?
Te daré una pista. La respuesta es muy corta y muy fácil. ¿Qué hacemos primero?
No, no reset
. Dibujamos. Eso es lo primero que debe hacer, ya que haría que todo lo demás fuera mucho más fácil. Así que este es el estado actual:
¿Y el estado deseado?
¿Cómo se pasa del estado actual al estado deseado, qué sería más fácil?
Entonces, una forma sería usar git reset
como lo hizo antes, pero hay otra forma que me gustaría que probara.
Primero, mueva HEAD
para señalar la rama existing
:
Intuitivamente, lo que desea hacer es tomar los cambios introducidos en la confirmación azul y aplicar estos cambios ("copiar y pegar") en la parte superior de la rama existing
. Y Git tiene una herramienta solo para eso.
Para pedirle a Git que tome los cambios introducidos entre esta confirmación y su confirmación principal y simplemente aplique estos cambios en la rama activa, puede usar git cherry-pick
. Este comando toma los cambios introducidos en la revisión especificada y los aplica a la confirmación activa.
También crea un nuevo objeto de confirmación y actualiza la rama activa para apuntar a este nuevo objeto.
En el ejemplo anterior, especifiqué el identificador SHA-1 de la confirmación creada, pero también podría usar git cherry-pick main
, ya que la confirmación cuyos cambios estamos aplicando es la main
a la que apunta.
Pero no queremos que estos cambios existan en la rama main
. git cherry-pick
solo aplicó los cambios a la rama existing
. ¿Cómo puedes eliminarlos de main
?
Una forma sería switch
a main
y luego usar git reset --hard HEAD~1
:
¡Lo hiciste! 💪🏻
Tenga en cuenta que git cherry-pick
en realidad calcula la diferencia entre la confirmación especificada y su padre, y luego los aplica a la confirmación activa. Esto significa que, a veces, Git no podrá aplicar esos cambios, ya que puede generar un conflicto, pero ese es un tema para otra publicación.
Además, tenga en cuenta que puede pedirle a Git que cherry-pick
los cambios introducidos en cualquier confirmación, no solo las confirmaciones a las que hace referencia una rama.
Hemos adquirido una nueva herramienta, por lo que tenemos git reset
y git cherry-pick
en nuestro haber.
Bien, otro día, otro repositorio, otro problema.
Crear un compromiso:
Y push
al servidor remoto:
Um, ups 😓...
Acabo de notar algo. Hay un error tipográfico allí. Escribí This is more tezt
en lugar de This is more text
. ¡Vaya! Entonces, ¿cuál es el gran problema ahora? push
ed, lo que significa que alguien más podría haber pull
esos cambios.
Si anulo esos cambios usando git reset
, como lo hemos hecho hasta ahora, tendremos diferentes historias y todo podría desatarse. Puede reescribir su propia copia del repositorio tanto como desee hasta que lo push
.
Una vez que push
el cambio, debe estar muy seguro de que nadie más haya obtenido esos cambios si va a reescribir el historial.
Alternativamente, puede usar otra herramienta llamada git revert
. Este comando toma la confirmación que le proporcionas y calcula la diferencia a partir de su confirmación principal, al igual que git cherry-pick
, pero esta vez calcula los cambios inversos.
Entonces, si en la confirmación especificada agregaste una línea, lo contrario eliminaría la línea y viceversa.
git revert
creó un nuevo objeto de confirmación, lo que significa que es una adición al historial. Al usar git revert
, no reescribió el historial. Admitiste tu error pasado, y este compromiso es un reconocimiento de que cometiste un error y ahora lo arreglaste.
Algunos dirían que es la forma más madura. Algunos dirían que no es un historial tan limpio como el que obtendrías si usaras git reset
para reescribir la confirmación anterior. Pero esta es una forma de evitar reescribir la historia.
Ahora puede corregir el error tipográfico y confirmar de nuevo:
Su caja de herramientas ahora está cargada con una nueva herramienta brillante, revert
:
Trabaja un poco, escribe un código y agrégalo a love.txt
. Realice este cambio y confírmelo:
Hice lo mismo en mi máquina, y usé la tecla de flecha Up
en mi teclado para desplazarme hacia atrás a los comandos anteriores, y luego presioné Enter
y... Wow.
¡Vaya!
¿Acabo de usar git reset --hard
? 😨
¿Qué sucedió realmente ? Git movió el puntero a HEAD~1
, por lo que no se puede acceder a la última confirmación, con todo mi precioso trabajo, desde el historial actual. Git también eliminó todos los cambios del área de preparación y luego hizo coincidir el directorio de trabajo con el estado del área de preparación.
Es decir, todo coincide con este estado en el que mi trabajo se ha... ido.
Tiempo de enloquecer. enloqueciendo
Pero, realmente, ¿hay alguna razón para enloquecer? No realmente… Somos gente relajada. qué hacemos? Bueno, intuitivamente, ¿se ha ido realmente el compromiso? ¿No por qué no? Todavía existe dentro de la base de datos interna de Git.
Si supiera dónde está, sabría el valor SHA-1 que identifica este compromiso, podríamos restaurarlo. Incluso podría deshacer la deshacer y reset
de nuevo a este compromiso.
Entonces, lo único que realmente necesito aquí es el SHA-1 del compromiso "eliminado".
Entonces la pregunta es, ¿cómo lo encuentro? ¿Sería útil git log
?
Bueno en realidad no. git log
iría a HEAD
, que apunta a main
, que apunta a la confirmación principal de la confirmación que estamos buscando. Luego, git log
rastrearía a través de la cadena principal, que no incluye la confirmación con mi valioso trabajo.
Afortunadamente, las personas muy inteligentes que crearon Git también crearon un plan de respaldo para nosotros, y se llama reflog
.
Mientras trabaja con Git, cada vez que cambia HEAD
, lo que puede hacer usando git reset
, pero también otros comandos como git switch
o git checkout
, Git agrega una entrada al reflog
.
¡Encontramos nuestro compromiso! Es el que comienza con 0fb929e
.
También podemos relacionarnos con él por su "apodo": HEAD@{1}
. Por ejemplo, Git usa HEAD~1
para llegar al primer padre de HEAD
, y HEAD~2
para referirse al segundo padre de HEAD
y así sucesivamente, Git usa HEAD@{1}
para referirse al primer padre reflog de HEAD
, donde señaló HEAD
en el paso anterior.
También podemos pedirle git rev-parse
que nos muestre su valor:
Otra forma de ver el reflog
es usar git log -g
, que le pide a git log
que realmente considere el reflog
:
Vemos arriba que reflog
, al igual que HEAD
, apunta a main
, que apunta a "Commit 2". Pero el padre de esa entrada en el reflog
apunta a "Commit 3".
Entonces, para volver a "Commit 3", solo puede usar git reset --hard HEAD@{1}
(o el valor SHA-1 de "Commit 3"):
Y ahora, si git log
:
¡Salvamos el día! 🎉👏🏻
¿Qué pasaría si volviera a usar este comando? ¿Y ejecutó git commit --reset HEAD@{1}
? Git configuraría HEAD
a donde apuntaba HEAD
antes del último reset
, es decir, "Commit 2". Podemos seguir todo el día:
Mirando nuestra caja de herramientas ahora, está cargada de herramientas que pueden ayudarlo a resolver muchos casos en los que las cosas salen mal en Git:
Con estas herramientas, ahora comprenderá mejor cómo funciona Git. Hay más herramientas que te permitirían reescribir el historial específicamente, git rebase
), pero ya aprendiste mucho en esta publicación. En publicaciones futuras, también me sumergiré en git rebase
.
La herramienta más importante, incluso más importante que las cinco herramientas enumeradas en esta caja de herramientas, es comparar la situación actual con la deseada. Confía en mí, hará que cada situación parezca menos desalentadora y la solución más clara.
También di una charla en vivo cubriendo el contenido de esta publicación. Si prefiere un video (o desea verlo junto con la lectura), puede encontrarlo
En general,
Omer tiene una Maestría en Lingüística de la Universidad de Tel Aviv y es el creador de la
Publicado por primera vez aquí