Usando Git para manipular el directorio de trabajo, el índice y commits (segunda parte)

Iniciado por MinusFour, 30 Noviembre 2020, 20:43 PM

0 Miembros y 1 Visitante están viendo este tema.

MinusFour

Indice

Prefacio

En este tema se explora un poco más acerca de como interactuar con el directorio de trabajo, el índice y los commits. Es una continuación de este tema.

Como recordatorío, esta es una guía informal a git.

El flujo de trabajo de git

Creo que para ahora debe quedar claro un poco como debería uno trabajar con git. En resumen, uno trabaja sobre los archivos en su directorio de trabajo. Los cambios que se realicen sobre el directorio de trabajo deben de ser agregados al índice, si son cambios que queremos guardar en nuestro repositorio y finalmente uno debe hacer commit para tomar los cambios agregados al índice y hacerlos permanentes al repositorio.

A grandes rasgos, podríamos decir que son 3 etapas. En este diagrama podemos ver que en nuestro ejemplo que hemos trabajado tenemos 2 archivos en nuestro directorio de trabajo (recordad que .git es especial), nuestro índice con los últimos cambios agregados y finalmente el commit que contiene todos los archivos agregados a través del índice más la información del autor, el ancestro directo del commit y el mensaje que le hemos dado.



El formato del índice no es importante por ahora, son detalles específicos de como opera git y quizás podamos ver más acerca de los objetos de git en un futuro. Por lo pronto es nuestro interés poder interactuar correctamente con estas tres etapas.

El directorio de trabajo

Como ya hemos dicho algunas 3 o 4 veces, el directorio de trabajo alberga nuestros archivos en los que estamos trabajando. Es a través de estos archivos que podemos abrirlos desde un editor de texto, ejecutarlos, etc. Hasta ahora git simplemente ha usado el directorio de trabajo para rastrear cambios sobre archivos que están marcados para rastrear. ¿Que más puede hacer git con nuestro directorio de trabajo?

¡Nuestro directorio de trabajo puede ser nuestra ventana al pasado y al futuro! Por ejemplo, podemos decirle a git que nos proporcione nuestro código fuente en un determinado commit (a través de una rama o simplemente usando el identificador del commit).

Ejemplo:

Para este ejemplo, vamos a jugar un poco con nuestro repositorio de prueba y vamos a revisar commits anteriores. Por lo pronto, verificamos el contenido actual de nuestro repositorio.



Como podemos ver, estamos en la rama master, la cual está apuntando al commit 0ba7347 (como dice git log), tenemos 2 archivos con su contenido como tal y 3 commits.



Importante:

Es importante recordar que los identificadores que ven aquí no serán los mismos que ustedes tendrán, tienen que revisar sus identificadores con git log como lo he descrito en la imagen.



Ahora, volvamos un poco al tiempo a nuestro primer commit (en mi caso es de00cee). Para esto usare una herramienta de git:

Código (bash) [Seleccionar]
git checkout



Y como podemos observar, git ha hecho el cambio y ahora nos está avisando que entramos en el modo detached HEAD. La advertencia nos dice que los cambios que realicemos en este modo no se conservarán normalmente pero si nos dice como podríamos conservar esos cambios. Por ahora trataremos nuestro directorio de trabajo como si solo tuviera permisos de lectura y no de escritura. Es decir, no deberíamos hacer ningún cambio en este estado por ahora.

Revisamos lo que ha hecho git checkout:



Y como podemos ver, nos ha modificado nuestro directorio de trabajo. Ya no tenemos nuestro archivo prueba.txt y nuestro archivo README.md es diferente, puesto que es la primera versión de nuestro archivo. También podemos ver que al revisar el estado del índice, nos dice que estamos en el modo especial mencionado anteriormente. git log nos dice que solo tenemos un commit y podemos ver que HEAD no está apuntando a master. ¿Nos hemos cargado los commits?

No, los commits siguen ahí y podemos revisarlos usando --all:



git simplemente ha sustituido nuestro directorio de trabajo por como se veía hace 2 commits. También ha tocado el índice para reflejar el estado de nuestro directorio de trabajo.

Revisaremos nuestro segundo commit:



Y ahora no nos ha dado la advertencia (porque seguimos en el mismo estado especial). Vamos a revisar nuevamente el commit:



Y no ha cambiado mucho en este commit, realmente solo hicimos cambios a un solo archivo.

Por ahora regresemos a colocar la cabeza en master:

Código (bash) [Seleccionar]
git checkout master

Es importante notar que seguiríamos en detached HEAD si usaramos el identificador del commit al que apunta master. Tenemos que usar el nombre de la rama para colgar la cabeza correctamente.

Como pudimos observar, git checkout nos ha reproducido exactamente las copias de los archivos que hemos guardado a través de los commits y nos los ha mostrado usando el directorio de trabajo. ¿Que pasaría si yo tuviera cambios pendientes y le pidiera que me mostrase algún commit o rama? Probemos.

Hare un cambio sobre prueba.txt. En este caso, no importa el cambio. Solo que ha ocurrido un cambio. Ustedes lo pueden simular editando el archivo de prueba.txt y haciendo cualquier cambio. Yo usare este comando para editar el archivo:

Código (bash) [Seleccionar]
echo 'a' >> prueba.txt



Como pueden ver, estoy de vuelta en la rama master como lo dice git status y me muestra que hay un cambio pendiente a prueba.txt que necesito agregar al indice. Supongamos que quiero revisar los contenidos del primer commit. ¿Que pasaría con los cambios que he hecho sobre el archivo prueba.txt? En ese punto del tiempo, no existía prueba.txt.



git nos avisa que no podemos hacer ese cambio porque el archivo sería sobrescrito. Nos dice que agreguemos los cambios al repositorio o que los guardemos en algún otro lado temporalmente (git tiene sus mecanismos para esto, pero lo veremos más tarde).

Esto significa que git checkout sobre un commit o rama es bastante seguro y nos advierte de cualquier peligro.

Ahora, git checkout también tiene otras utilidades, por ejemplo podemos revisar archivos en específico de otros commits. La notación no es muy diferente:

Código (bash) [Seleccionar]
git checkout rama/commit /path/a/archivo

Aunque es preferible usar un -- en medio para evitar confusiones en los argumentos:

Código (bash) [Seleccionar]
git checkout rama/commit -- /path/a/archivo

Con este comando podríamos obtener el archivo README.md hace 2 commits:



Ahora git status detecta que hubo un cambio sobre el archivo README.md porque me he traído la primera versión. Y no solo eso, me la ha agregado al índice automáticamente.

Digamos que quiero regresar a la última versión del archivo, tengo tres opciones para hacer esto:

Código (bash) [Seleccionar]

git checkout HEAD -- README.md
git checkout master -- README.md
git checkout 0ba7 -- README.md


Recordemos que HEAD en este momento apunta a master que este apunta a 0ba7, el último commit de la rama, la punta. El primer comando no funcionaría si estuviese revisando el contenido de otro commit o rama, puesto que HEAD no estaría apuntando a master.



¿Pero que pasaría si tuviera cambios nuevos e hiciera git checkout rama/commit -- /path/a/archivo?



git no me ha avisado de cambios que pudiera perder. No, simplemente ha sobrescrito mi archivo. Es muy importante tener cuidado cuando utilicen git checkout de está forma, ya que podrían perder cambios importantes. Sin embargo, revisar las ramas o commits directamente es seguro puesto que git no nos deja hacer el cambio.

Otro punto importante es que git checkout restablece archivos directamente del índice si no se le especifica una rama o commit en esta forma. Lo que significa que si tenemos cambios sin agregar al índice podemos regresar al estado anterior (al del último commit). En nuestro caso ahora mismo tenemos agregado el archivo README.md al índice, por eso si hacemos:

Código (bash) [Seleccionar]
git checkout README.md

No obtendremos ningún cambio sobre nuestro directorio de trabajo, puesto que git checkout está obteniendo la última copia del índice, la cual fue agregada al índice por la invocación anterior puesto que git checkout rama/commit -- /path/a/archivo no solo obtiene el archivo de la rama/commit sino que también la agrega al índice.



Sin embargo, si hiciéramos un cambio nuevo sobre este archivo:



Aquí podemos ver que he agregado cambios sobre el archivo README.md al índice pero tengo cambios pendientes todavía no agregados al índice. Si usará el comando:

Código (bash) [Seleccionar]
git checkout README.md

Conservaría los cambios agregados al índice pero me desharía de los cambios que todavía no son parte del índice.



Como pueden ver en la sección inferior del git status ya no aparece que se haya modificado README.md puesto que ha vuelto a la versión que está registrada en el índice.

Por lo pronto quiero deshacerme de estos cambios en mi directorio de trabajo (que están agregados al índice por cierto) y volver a la última versión que tengo, simplemente usaré la otra versión del comando que use anteriormente (para demostrar que es lo mismo).

Código (bash) [Seleccionar]
git checkout master -- README.md



Y así, nuestro archivo README.md ha vuelto a su última versión tanto en el directorio de trabajo como en el índice.

git checkout tiene más utilidades que veremos más adelante. Por lo pronto, expandiremos en un comando el cual se utiliza mucho en conjunto con este, git stash.

Como recordarán, git checkout nos advierte que tenemos cambios sin haber sido registrados en un commit aún y no nos dejará cambiar de rama/commit si sigue detectando que existen estos cambios. Nos ha dado dos alternativas, realizar un commit con los cambios o guardar los cambios en algún lugar. Como es muy posible que tengas que dejar de trabajar sobre la rama en la que estás trabajando, git tiene un comando que nos permite guardar estos cambios y aplicarloss de vuelta cuando sea necesario. El comando es:

Código (bash) [Seleccionar]
git stash

Yo llamaría git stash otra herramienta indispensable para trabajar con el directorio de trabajo. Por defecto, git stash tomará los cambios de archivos modificados y los substituirá por las versiones del commit al cual apunta HEAD. En pocas palabras, nuestros archivos son los mismos que aparecen en nuestro commit.

En nuestro ejemplo anterior, podemos ver como tenemos archivos pendientes con git status sobre prueba.txt. Por lo pronto vamos a guardar esos cambios:



Nuestros cambios ahora han desaparecido y podemos hacer el cambio a cualquier otra rama/commit con git checkout ya que no tenemos modificaciones pendientes a agregar.

¿Pero que ha pasado con nuestros cambios? Nuestros cambios ahora son un stash. Podemos ver los stash que tenemos usando:

Código (bash) [Seleccionar]
git stash list



Podemos ver nuestro stash como stash@{0} con su mensaje por defecto. De hecho, inclusive podríamos hacer git checkout sobre este identificador. Pero no es el uso normal. Cuando uno necesita poder acceder a los cambios tenemos dos opciones:

1) Aplicar los cambios y borrar el stash (yo diría que el uso más común)
2) Aplicar los cambios y conservar el stash.

Para aplicar los cambios y borrar el stash podemos usar:

Código (bash) [Seleccionar]
git stash pop

Para aplicar los cambios y conservar el stash simplemente usamos:

Código (bash) [Seleccionar]
git stash apply

Para borrar el stash:

Código (bash) [Seleccionar]
git stash drop

Por defecto, estos comandos operan sobre el stash más reciente, pero puedes especificar el identificador del stash si tienes otros stash con los cuales quieres trabajar.

Por ahora, aplicare git stash pop sobre mi directorio de trabajo y regresare al estado original (con las modificaciones pendientes sobre prueba.txt):



Y volvemos al estado original, podemos ver que git nos dice que también ha borrado el stash usando git stash list (al final del comando lo confirma también).

Detalles importantes a recordar acerca de git stash:

1) Por defecto, no conserva los archivos que no han sido rastreados, e.g. archivos nuevos que nunca han sido agregados. Se necesita especificar el argumento -u:

Código (bash) [Seleccionar]
git stash -u

2) Por defecto al aplicar los cambios no agrega inmediatamente los cambios sobre el índice. Para eso tienes que usar:

Código (bash) [Seleccionar]
git stash pop --index

O simplemente volver a agregar los cambios al índice una vez que hayas hecho git stash pop o git stash apply:

Código (bash) [Seleccionar]
git add .

Existen otros comandos que también modifican el directorio de trabajo pero en mi opinión estos dos comandos son unos de los más importantes y de los más usados. Estos comandos también interactúan con el índice pero su uso principal es sobre el directorio de trabajo.

El índice

Manipular el índice es otra de las tareas importantes para el usuario de git. Es a través del índice que uno establece que cambios son considerados para ser archivados en un commit. Exploremos un poco más acerca de como funciona el índice de git. Como habíamos dicho anteriormente, el índice de git nos sirve para rastrear posibles cambios y agregarlos posteriormente a un commit. Cuando agregamos un archivo al índice, git convierte dicho archivo a un objeto interno (el cual también es un archivo pero con un formato especial) y mantiene el registro del cambio. Hasta ahora, quizás tengamos una idea que el índice es una larga lista de cambios a ser agregados pero en realidad el índice mantiene una lista de objetos (donde cada objeto es de hecho el archivo en su totalidad pero comprimido). Cuando nosotros creamos un commit, git usa estos objetos en el índice para crear el nuevo commit.

El índice, al igual que el directorio de trabajo, está en constante cambio. No tenemos un índice nuevo entre cada commit. Cuando creamos un commit, el índice sigue teniendo los mismos objetos. Muchos otros comandos van a cambiar el índice, por ejemplo, el comando git checkout rama/commit hará cambios sobre el directorio de trabajo y el índice reflejara los contenidos de este commit. De esa manera, el índice puede verificar cuando existen cambios o no. En un principio, puede sonar extraño mover archivos entre el índice y el directorio de trabajo, pero es importante recordar que el índice mantiene información COMPLETA acerca de nuestros archivos puesto que no está muy lejos de poder convertirse en un commit (y son estos mismos objetos los que acaban en el commit). Recordemos que dado un solo commit, git es capaz de extraer tu código exactamente como lo has dejado a la hora de hacer el commit.

Ejemplo:

Los siguientes ejemplos son para ejemplificar el funcionamiento del índice de git. En nuestro ejemplo anterior habíamos dejado pendientes modificaciones sobre prueba.txt:



Lo que podemos inferir de esto es que el índice en este momento está manteniendo un objeto para el archivo prueba.txt correspondiente al mismo objeto que usa nuestro último commit el cual podemos identificar fácilmente con HEAD (que a estás alturas, deberíamos ya saber que apunta a master y este apunta al último commit). ¿Como es que sabemos esto? No hay cambios agregados al índice todavía, solo cambios pendientes por agregar al índice.

Podemos verificarlos con una serie de comandos internos de git:



El primer comando nos muestra el contenido del índice, en el cual tenemos un objeto para prueba.txt bajo el identificador SHA-1 6de3. Podemos leer el objeto y obtenemos el mismo contenido que existe tal cual en el último commit. Sin embargo, ¿Realmente estamos usando el mismo objeto? Para satisfacer la curiosidad de algunos cuantos, el último comando imprime el contenido tal cual está registrado en nuestro último commit (nuevamente, HEAD). Como podemos observar, no solo prueba.txt está usando el mismo objeto, sino que también README.md. Lo cual tiene sentido porque no se ha agregado NADA al índice todavía, tenemos un cambio pendiente sobre prueba.txt solamente. Así podemos comprobar que el índice, en este momento, contiene exactamente lo mismo que nuestro último commit.

Ahora, vamos a agregar los cambios al índice:



¡Y ahora nuestro índice cambio! Lo que ha hecho git es crear un nuevo objeto con el contenido de nuestro archivo y ahora el índice usa este nuevo objeto. Una vez que hagamos git commit, git usara estos nuevos objetos para construir dicho commit. En ese momento, el nuevo commit y el índice volverán a usar los mismos objetos, por lo cual no hay nuevos cambios (hasta que agreguemos nuevas cosas).

Ahora, quiero continuar explicando un poco más acerca de el índice de git. Por lo que usare este siguiente comando para volver antes de la explicación (cuando teniamos cambios pendientes en prueba.txt).

Usare el siguiente comando:

Código (bash) [Seleccionar]
git reset prueba.txt



Un nuevo comando del cual todavía no sabemos nada, el cual explicaremos ahora.

Interactuando con el índice

En primer lugar, vamos a recapitular el estado de nuestro índice y nuestro directorio de trabajo. Lo primero que tenemos que preguntarnos es: ¿Nuestro directorio de trabajo ha cambiado en estos últimos comandos? Fuera de los comandos internos que he usado para ejemplificar el índice (no hacen cambios sobre el directorio de trabajo), los únicos comandos que hemos usado hasta ahora son git add y git reset. Y no, estos dos comandos no han alterado nuestro archivo prueba.txt en nuestro directorio de trabajo. Sin embargo, git add y git reset si han cambiado nuestro índice. git add agrego un nuevo objeto al índice para nuestro archivo prueba.txt, mientras que git reset, como se lo pueden imaginar, ha hecho exactamente lo contrario.

El comando en su "forma completa" usa:

Código (bash) [Seleccionar]
git reset rama/commit -- /path/a/archivo/

Lo cual les puede resultar muy familiar al comando:

Código (bash) [Seleccionar]
git checkout rama/commit -- /path/a/archivo/

¿Cual es la diferencia? Recordamos que git checkout rama/archivo -- /path/a/archivo/ cambiaba el archivo en nuestro directorio de trabajo y lo agregaba al índice. Si no especificamos la rama/commit, git checkout usará el índice para cambiar el directorio de trabajo. Esto quiere decir que:

Código (bash) [Seleccionar]
git checkout prueba.txt

Hubiese tomado una copia del índice y después la agregaría al directorio de trabajo. La transición es del índice al directorio de trabajo. Sin embargo:

Código (bash) [Seleccionar]
git reset prueba.txt

Ha hecho casi lo contrario: Ha tomado una copia del archivo de HEAD y la ha movido al índice. La transición es de HEAD al índice. Por otro lado:

Código (bash) [Seleccionar]
git add prueba.txt

Haría algo también muy similar a git checkout y git reset. En este caso, git add movería el archivo del directorio de trabajo al índice. La transición es de directorio de trabajo al índice.

¿Que significa esto?

git checkout estaría alterando nuestro directorio de trabajo (el archivo sobre el cual estamos editando). En está función (sin especificar un commit/rama), está principalmente ELIMINANDO los últimos cambios que no han sido agregados al índice. Lo que se haya agregado al índice no será eliminado. Por otro lado, si se especifica una rama/commit, los contenidos guardados en el índice si se perderán. git checkout HEAD -- prueba.txt, eliminaría ambos cambios tanto del índice como del directorio de trabajo.

git reset estaría alternado el índice y no tocara el directorio de trabajo en lo absoluto. Si no especificamos la rama/commit, tomará el archivo de HEAD y remplazará cualquier cambio que exista sobre el índice. El índice vuelve al estado que tenía en HEAD. Especificar la rama, para establecer el índice no es un caso típico. Pero sería muy similar a lo que hace git checkout con la excepción que no modificará el directorio de trabajo. Realmente, es de lo más raro especificar una rama/commit porque en muchas formas es el equivalente de git add pero para agregar archivos de otros commits (es más como un git index-set, comando que no existe pero eso es lo que hace...).

Finalmente, como git reset no ha alterado el directorio de trabajo, podemos simplemente agregar el archivo nuevamente al índice. git reset y git add son considerados opuestos (cuando no se especifica la rama/commit en git reset).

Podríamos decir que git checkout y git reset son los comandos más confusos que existen en git. Porque ambos hacen cosas similares y al mismo tiempo pueden hacer cosas totalmente diferentes. Por ejemplo, ya hemos visto que git checkout también puede hacer cambios sobre HEAD y obtener los archivos de un commit/rama para desplegarlos en el directorio de trabajo. git reset también tiene otra función que veremos más adelante.

Ejemplo:

Vamos a probar cada uno de los casos de uso de estos tres comandos y utilizare varios ejemplos. Primero, establecer el punto del cual parte estos comandos:



No hemos hecho ningún commit desde el tema anterior, lo cual significa que si se han perdido pueden volver al tema anterior y seguir los pasos nuevamente. Lo único que faltaría sería editar el archivo prueba.txt como ustedes deseen. Por otro lado, si han seguido todos los ejemplos (y mis instrucciones) deberían tener los mismos resultados que yo.

Ahora, lo primero que haremos será agregar nuevamente los cambios sobre prueba.txt al índice. Usaremos:

Código (bash) [Seleccionar]
git add .



Esta vez no hemos usado el nombre del archivo, sino que le hemos dicho a git que agregue todos los cambios detectados sobre el directorio actual (. representa nuestro directorio actual, notación común en sistemas linux). Ahora probaremos:

Código (bash) [Seleccionar]
git checkout prueba.txt



¿Que ha ocurrido? Nada. Recordemos que git checkout toma los cambios del índice y los usa para el archivo en el directorio de trabajo. Nuestro archivo en el directorio de trabajo es exactamente igual al del índice, lo que significa que no hay cambios. ¿Pero que pasaría si agregara un cambio?



Ha eliminado los últimos cambios sobre el directorio de trabajo, ya que el índice tenía una versión anterior a los cambios que acabamos de hacer sobre el directorio de trabajo. ¿Y que pasaría si usara HEAD como argumento para la rama/commit?



Adios cambios. git checkout ha tomado el archivo de HEAD, lo ha puesto en nuestro directorio de trabajo (eliminando los cambios que tenía) y también los ha puesto en el índice. ¿Resultado? Adios cambios.

Volveremos a agregar cambios con:

Código (bash) [Seleccionar]
echo 'a' >> prueba.txt

Y agregaremos una vez más el archivo con git add



Ahora, probaremos git reset:



Nuestro archivo prueba.txt sigue teniendo el mismo contenido antes y después del comando. Pero el índice muestra que los cambios agregados anteriormente han sido removidos.

El estado de los archivos con respecto al índice

Cada vez que agregamos un archivo o modificamos un archivo, el comando git status nos advierte de el estado del archivo con respecto al índice. Si creamos un archivo que no existe en el índice, ese archivo se dice que está untracked. En español significa que el archivo está sin rastrear. git no está rastreando cambios al archivo. Cuando agregamos el archivo al índice, el archivo ahora si es rastreado. ¿Pero porque es importante esto?

Es importante porque algunos de los comandos trabajan sobre el índice y el estado de los archivos en el índice va a determinar que es lo que hace cada comando. Para el índice un archivo puede ser:

A) Nuevo (sin rastrear)
B) Modificado (rastreado)
C) Borrado (rastreado)

Por ejemplo, git checkout rama/commit no tocará archivos que no han sido rastreados. Recordarán que git checkout arroja un error al intentar revisar un commit o rama si hay modificaciones pendientes. Sin embargo, git checkout rama/commit no arrojara ningún error sobre nuevos archivos que no hayan aparecido antes. Esta operación no solo nos entregará los archivos registrados en el commit sino que estos nuevos archivos que no han sido rastreados también aparecerán en conjunto en el directorio de trabajo.

De la misma forma hay algunos comandos que tendrán argumentos que trabajarán dependiendo de su estado en el índice. Por ejemplo, git add -u actualizará solo archivos rastreados pero no agregará archivos sin rastrear.

Por último, un archivo sin rastrear puede ser también ignorado por el índice. Esto significa que git status no se molestará en decirte nada acerca de estos archivos, pero siguen siendo prácticamente archivos sin rastrear.

Ejemplo:

Volveremos a usar el mismo estado que hemos usado por los últimos ejemplos:



Como podemos observar, prueba.txt es un archivo rastreado. Vamos a crear un segundo archivo, al cual simplemente llamaremos nuevo.txt:

Código (bash) [Seleccionar]
echo 'soy nuevo' > nuevo.txt

Y verificamos nuevamente el estado del índice:



Podemos ver que tenemos un archivo sin rastrear (nuevo) y uno de nuestros archivo rastreados ha sido modificado. Ahora mismo voy a borrar un archivo que es rastreado:



Ahora probaremos git add -u:



Y nos ha agregado solo los archivos que son rastreados pero no los archivos sin rastrear. Sin embargo, si usamos git add -A o git add . :



Ha agregado el archivo sin rastrear.

El argumento -A de git add es para agregar todos los cambios en todo el directorio de trabajo. Mientras que git add . agrega todos los cambios (de archivos rastreados y no rastreados) sobre la carpeta en la que estamos. Si la carpeta es la raíz, entonces los dos hacen lo mismo. Había más diferencias en versiones anteriores de git en la cual git add -A era el único que añadía todos los cambios. La mayoría prefiere usar este argumento (en el caso que necesiten agregar todos los cambios) debido a esto.

Digamos que quiero regresar al estado inicial, en el cual solo tenía cambios pendientes sobre prueba.txt ¿Que comandos debería usar? Ahora mismo, hemos hecho 3 cambios sobre nuestro directorio de trabajo. Hemos borrado README.md, tenemos un nuevo archivo nuevo.txt y hemos cambiado prueba.txt. De igual forma, tenemos 3 cambios sobre el índice también. Exactamente los mismos cambios que existen en nuestro directorio de trabajo. Vamos a borrar los cambios sobre README.md:

Código (bash) [Seleccionar]
git checkout HEAD -- README.md



Ha obtenido la copia de HEAD, la ha traido a nuestro directorio de trabajo y la ha puesto en el índice también.

Ahora quiero eliminar los cambios de prueba.txt en el índice pero quiero conservar los cambios en mi directorio de trabajo. Un trabajo para git reset:

Código (bash) [Seleccionar]
git reset HEAD -- prueba.txt



Simplemente ha puesto en el índice la versión de HEAD, dejando los cambios en el directorio de trabajo intactos. Ahora git status nos dice que tenemos cambios pendientes sobre prueba.txt

Finalmente, ¿Que podemos hacer sobre nuestro archivo nuevo.txt? No podemos usar git checkout para eliminar estos cambios sobre nuestro directorio de trabajo:



Curiosamente, git reset si podría borrar el archivo del índice a pesar que no existe el archivo en HEAD, sin embargo tendríamos todavía que eliminar el archivo del directorio de trabajo. También podríamos simplemente removerlo del directorio de trabajo y añadir el cambio nuevamente al índice. Básicamente, dos comandos para realizar está operación.

Sin embargo, tenemos otra opción que puede hacer las dos cosas al mismo tiempo:

Código (bash) [Seleccionar]
git rm

Este comando, hará las dos cosas, eliminará el archivo del índice y del directorio de trabajo.



Pero para nuestra desgracia, el comando ha fallado aquí. ¿La razón? git rm es bastante inteligente acerca de borrar cosas. En este caso, git rm ha identificado que este archivo es nuevo y que tenemos cambios a perder si borramos el archivo del directorio de trabajo. Por eso nos sugiere --cached, en caso de que queramos conservar el archivo en el directorio de trabajo pero quitarlo del índice o si estás seguro que quieres borrar ambos archivos puedes usar -f. Como estamos seguros que queremos quitar el archivo usaremos este argumento.



Ahora, nos ha eliminado el archivo por completo. Tanto del índice como del directorio de trabajo. Y hemos vuelto al estado original.

¿Que pasaría si usara git rm para eliminar algún archivo que tengo rastreado? Nuestro archivo no rastreado que estaba en el índice y directorio de trabajo, acabo con 0 cambios posibles sobre el índice. Podemos ver que no hay cambios a agregar al índice sobre un archivo nuevo.txt ni cambios agregados al índice sobre nuevo.txt.

Intentaremos borrar README.md, ¿Que pasaría con el índice?



Noten como ahora no hubo necesidad de usar -f. Ya que no existen cambios a perderse, estos están en HEAD. Si hubiese modificado README.md, git rm me hubiese adveritdo de lo mismo, que hay cambios posibles por perderse. En ambas ocasiones, tanto para nuevo.txt como para README.md ambos archivos fueron eliminados tanto del índice como del directorio de trabajao. Más sin embargo, el resultado de git status es diferente. ¿Porque he agregado un cambio al índice con README.md y no queda rastro alguno sobre nuevo.txt?

La razón es simple. HEAD, nuestro último commit, tiene una copia de README.md por lo que eliminar el archivo del índice produciría un cambio. Sin embargo, nuevo.txt no existe en HEAD es un archivo nuevo, si agrego el archivo al índice produciré un cambio ya que no existe anteriormente. Pero si no existe en el índice no hay ningún cambio pendiente porque no existe en HEAD en primer lugar.

Vamos a restablecer nuestro archivo README.md como lo hemos hecho anteriormente.



A seguir trabajaremos el caso de uso para mover archivos. ¿Suena sencillo no? Primero agregaremos prueba.txt al índice nuevamente. Creo que ahora deben saber al menos unas 4 diferentes formas en las que pueden agregar el archivo:

Código (bash) [Seleccionar]
git add -A
git add -u
git add .
git add prueba.txt




Digamos ahora que quiero cambiar el nombre de prueba.txt a muestra.txt. Lo intentaremos hacer sobre el directorio de trabajo con:

Código (bash) [Seleccionar]
mv prueba.txt muestra.txt



¿Que ha pasado? git piensa que he borrado un archivo del directorio de trabajo y he creado uno nuevo. Esto es técnicamente cierto, prueba.txt no existe y ahora existe muestra.txt. Primero eliminare el archivo del índice. Tengo un par de opciones aquí. Podría agregar el archivo eliminado con git add. Si, así es, git add también hace eso.

Código (bash) [Seleccionar]
git add prueba.txt

También podría eliminar el archivo del índice con git rm --cached como nos sugirió en su momento git rm. Lo cual le da mucho más sentido a su nombre ya que no estamos agregando un archivo, estamos borrandolo.

Código (bash) [Seleccionar]
git rm --cached prueba.txt



Y también agregaremos el archivo muestra.txt al índice con git add.



Parece ser que git no es tan tonto como pensábamos. Ahora si ha deducido que el archivo prueba.txt ha sido renombrado a muestra.txt.  No solo eso, el archivo es ligeramente diferente de HEAD. Lo que significa que git ha hecho una comparación inteligente sobre nuestro archivo para deducir que muestra.txt era de hecho prueba.txt.

¿Tiene que existir una manera más sencilla de hacer esto, no? Pues la hay. Tenemos el comando git mv. Volveremos a poner muestra.txt como prueba.txt:



¿Mucho más sencillo no? Para seguir con los ejemplos, quitare los últimos cambios en el índice de prueba:

Código (bash) [Seleccionar]
git reset HEAD -- prueba.txt

Ignorando archivos sin rastrear

Llega un momento en el que una persona necesita crear archivos sobre el directorio de trabajo y no está interesado en agregarlos al repositorio. Por ejemplo, quizás no quieras tener binarios compilados de C en tu repositorio. Quizás necesites tener una contraseña en algún archivo dentro del directorio de trabajo y no quieres que esa contraseña acabe en el repositorio. ¿Algún archivo temporal en especifico que genere tu programa? Uno siempre puede ser especifico con los comandos a utilizar para manipular el índice (recuerden que si no está en el índice, nunca será parte de un commit, ni del repositorio) sin embargo es muy fácil errar y muy probablemente en algún punto del desarrollo tu o alguién se equivoquen, agreguen estos archivos al índice, dentro de un commit o un repositorio en linea.

Para evitar llegar a tener esos problemas, podemos simplemente establecer que archivos deben ser ignorados para que NUNCA acaben en el índice y mucho menos en un commit. ¿Como hacemos esto? Existen varias formas de hacer esto, pero por lo general se usa un archivo .gitignore en la raíz. Dentro de este archivo uno puede escribir patrones sobre los nombres de los archivos que queremos ignorar. Es recomendable que este archivo sea añadido al repositorio (a través de un commit).

Ejemplo:

Agregaremos un nuevo archivo, password.txt en el cual estará nuestracontraseña. Este archivo es importante para poder establecer una conexión con un servidor o algo similar.



Ahora git status nos advierte que tenemos un archivo nuevo a agregar. Digamos que agrego los cambios a prueba.txt con git add -A porque eso es lo que uso siempre antes de hacer git commit.



Tenemos un problema. Ahora nuestra contraseña está en el índice, lo cual significa que puede acabar en un commit. En esta ocasión me he dado cuenta, así que simplemente lo borrare del indice.

Código (bash) [Seleccionar]
git rm --cached password.txt
git reset HEAD -- password.txt




Ahora agregare un archivo .gitignore con el contenido password.txt:



Como pueden ver, password.txt está en el directorio de trabajo pero git status lo está ignorando. ¿Que pasará si intentamos agregar el archivo?



Absolutamente nada. Y si agregaríamos todos los cambios con git add -A tampoco funcionaría gracias a nuestro archivo .gitignore. En está ocasión he puesto el nombre completo del archivo, pero realmente podría usar patrones más generalizados. Por ejemplo podría ignorar archivos con una extensión o archivos dentro de una carpeta.

Para continuar con los ejemplos, me desharé de .gitignore y password.txt. Así como también quitare los cambios de prueba.txt sobre el índice:

Código (bash) [Seleccionar]
rm -f .gitignore password.txt
git reset HEAD -- prueba.txt


Trabajando con los commits

Si has llegado a esta parte del tutorial, no hay mucho más que decir acerca de los commits. La idea detrás de ellos es bastante simple. Lo único que queda agregar sobre los commits es como trabajar con ellos, los diferentes casos de usos y explicar como trabajan las herramientas.

git commit

No queda mucho más que decir de este comando. git commit tomará el índice y básicamente guardara todos los objetos que tenga el índice dentro de un commit. Tendremos que colocar un mensaje en nuestro editor configurado y se creara el commit. Tenemos un par de opciones útiles con el comando.

La primera es el argumento: -a

Código (bash) [Seleccionar]
git commit -a

Git tomará todos los cambios de archivos rastreados y los añadirá al índice antes de hacer el commit. Sin embargo, los nuevos archivos no serán agregados al índice con este argumento por lo que todavía tienen que agregar estos nuevos archivos por separado.

Otro argumento básico es:

Código (bash) [Seleccionar]
git commit -m "mensaje para git"

Bastante sencillo, el argumento -m nos permite especificar el mensaje del commit sin usar un editor de texto en la terminal. Hare una nota aquí acerca del formato de los mensajes en los commits.

Los mensajes de commits se usan para indicar que es lo que el commit está haciendo. Hay un número de reglas que se usan generalmente para producir un mensaje adecuado. Estás reglas no están reforzadas por lo que pueden usar cualquier mensaje pero es buena idea tomarlas en cuenta. No tiene mucho tiempo en el que estaba escribiendo un mensaje para un commit dentro de mi editor de texto (nvim) y mi editor empezo a darle un formato extraño al texto de mi mensaje con colores un poco raros. Al principio pense que mi editor de texto estaba fallando y no entendía que era lo que estaba haciendo.



No entendía que era lo que estaba pasando hasta que un día me dí cuenta que las herramientas que trabajan con git usan un formato en especifico para presentar los mensajes. Para ser más específicos, la primera linea del commit se le considera el título del commit. Las herramientas que imprimen mensajes breves acerca del commit, usarán la primera linea del mensaje. Se recomienda que la segunda linea del mensaje este vacía. Es un separador entre el titulo y el cuerpo del mensaje.

Finalmente, está el cuerpo del mensaje el cual es más libre. El único detalle realmente es que el cuerpo del mensaje no debería extenderse fuera de los 72 caracteres. Es decir, cualquier linea en el cuerpo no puede tener más de 72 caracteres.

Otras reglas implicitas sobre los mensajes pueden ser:

1) No usar punto para el título
2) El título debe empezar con letra mayúscula
3) El título debe describir lo que hace y poderse leer de tal forma que: "Este commit TITULO DEL MENSAJE AQUI" tenga sentido.
4) El cuerpo del mensaje debe explicar que es lo que hace y porque, no como lo hace.

De esta forrma, nuestros mensajes dejarán de verse así:





Y se verán así, mucho mejor organizados.





Si necesitan ser explícitos con sus mensajes, recomiendo usar su editor de texto preferido para escribir el mensaje. Podrían configurar git para usar el editor adecuado o pueden simplemente escribir el mensaje en un archivo y usar los contenidos de ese archivo como el mensaje.

Código (bash) [Seleccionar]
git commit -F archivoconmensaje.txt

Modificando commits

Llegará el día en que cometamos un error al hacer un commit. Quizás hemos escrito mal algo en el mensaje del commit. Quizás hemos agregado algo que no debíamos al commit. Quizás simplemente no queremos ninguno de estos commits. La realidad es que no podemos hacer modificaciones sobre un commit, no exactamente. Lo que podemos hacer es remplazar un commit por uno nuevo que contenga los cambios requeridos. Podrías preguntarte ¿Que importa si el commit no es el mismo si en un final tenemos el commit con el contenido que necesitamos? Y por ahora preferíría no ofrecer una respuesta hasta que empecemos a hablar acerca de colaborar con otros.

Ejemplo:

Crearemos un nuevo commit con los cambios que hemos venido conservando entre ejemplos:



He cometido un error intencional sobre mi mensaje de commit. ¿Como puedo cambiar el mensaje del commit?

Usaremos el comando:

Código (bash) [Seleccionar]
git commit --amend



Y ahora nuestro mensaje ha sido corregido. Sin embargo, podemos observar también que los identificadores de estos dos commits son diferentes. Uno dice 349f02b y el otro dice 811439d. Ambos contienen los mismos cambios pero realmente son dos diferentes commits.

¿Que ha pasado con nuestro commit 349f02b?



Nuestro commit existe y podemos revisarlo con git checkout y si revisamos git log nos muestra un historial muy similar. Lo que ha ocurrido es que git commit ---amend ha desplazado la rama master. Ha creado un nuevo commit cuyo ancestro es el mismo ancestro al que HEAD apuntaba y ha dicho que este nuevo commit es el nuevo master.

La transición la podemos representar de está manera.



El commit es inaccesible desde master pero sigue ahí. git eventualmente eliminará el commit (porque no hay forma de llegar al commit de ninguna rama) pero por lo pronto sigue ahí y podemos rescatar lo que queramos. Siempre y cuando sepamos el identificador del commit (y git no lo haya eliminado). ¿Pero y si necesito modificar más del commit que solo el mensaje?

Haciendo cambios sobre el historial de git

Tendremos que usar una herramienta que nos ofrece muchas posibilidades para recrear el commit. Y está herramienta ya la conocemos: git reset. Hasta ahora solo hemos usado git reset para restablecer el índice pero git reset es de hecho una herramienta mucho más versátil en cuanto a commits se trata.

Primero, tendremos que repasar un poco acerca del flujo de trabajo. Imaginemos que estamos trabajando sobre un repositorio por lo menos con un solo commit. Ahora, no hemos trabajado en lo absoluto sobre el directorio de trabajo y no hemos tocado el índice. En pocas palabras, nuestro directorio de trabajao refleja los mismos archivos que nuestro último commit y nuestro indice.

Si nosotros editáramos un archivo en nuestro directorio de trabajo, este ahora sería diferente a la copia que tenemos en nuestro último commit y el índice. Si agregaramos este archivo al índice, el indice ahora tendría la misma copia que el directorio de trabajo y el archivo en el commit sería diferente a la copia que tenemos en el directorio de trabajo y el indice. Finalmente, hacemos un commit y ahora este último commit tendrá la misma versión que nuestro directorio de trabajo y nuestro indice. Este es el flujo de trabajo de git y necesitas entenderlo para entender como funciona git reset.

Hasta ahora, hemos usado git reset en su forma git reset rama/commit -- /path/a/archivo, la cual en la mayoría de los casos se puede escribir git reset /path/a/archivo si queremos trabajar con HEAD por defecto. En esta forma, hacemos cambios exclusivos sobre el índice. Ahora, existe otra forma de usar git reset:

Código (bash) [Seleccionar]
git reset rama/commit

Y la forma es muy similar a la anterior. De hecho, este comando sobre HEAD haría lo que uno esperaría, que es quitar todos los cambios del índice. Piensa que si usas un archivo estarías quitando los cambios en el indice sobre un archivo, está forma está quitando todos los cambios de todos los archivos del índice. Pero ahora nuestro interes es ver que es lo que ocurre cuando especificamos otra rama/commit que no sea HEAD.

Esto es muy sencillo, git reset moverá la rama que estamos usando en HEAD al commit que le hemos dicho. De esta manera, no solo estamos moviendo la rama en cuestión sino también estamos moviendo HEAD indirectamente ya que HEAD apunta a una rama que apunta a un commit. Esto es lo primero que hará git reset rama/commit. Lo siguiente que hará git reset dependerá del modo que se haya elegido a trabajar git reset. Hay 6 modos descritos en el manual pero por ahora solo veremos 3.

Primero tenemos el modo --soft. En este modo, lo único que hace git reset es desplazar la rama al commit que le hayas dicho. Tu índice permanece igual, tu directorio de trabajo permanece igual, lo único que ha cambiado realmente es que la rama a la que apuntaba HEAD ahora está apuntando a otro commit.

Nuestro siguiente modo es --mixed. En este modo, git reset desplazará la rama y reiniciará el índice con los archivos del commit al que nos estamos desplazando. Tu directorio de trabajo permanece igual. Si no se especifica un modo, este será el modo que se utilice. De modo que git reset rama/commit lleva un --mixed implicito.

Finalmente, tenemos el modo --hard. En este modo, git reset desplazará la rama y dejará tanto el índice como el directorio de trabajo en el estado del commit al cual la rama ha sido desplazada.

Para visualizar estos cambios mejor usaremos gráficas:


En esta gráfica podemos ver el flujo de trabajo con git, que es lo que ocurre con con cada el índice y el directorio de trabajo en cada etapa de la edición de un archivo. Donde v1 es la primera versión del archivo y v2 es el archivo con los cambios aplicados. A la derecha de la gráfica, está el indicador de estos tres modos descritos. Estos describen el estado del índice y del directorio de trabajo al finalizar la operación. La operación también asume que hemos usado git reset primercommit como base desde un HEAD que apunta una rama que apunta al segundo commit.

También podemos ver que el resultado nos dejaría en ese determinado paso en nuestro flujo de trabajo. Es decir, --soft por ejemplo, nos dejaría con los mismos cambios en el índice y el directorio de trabajo. Prácticamente un punto antes de crear el commit. --mixed nos hubiera reiniciado el índice lo que significa que nos dejaría con nuestros archivos en el directorio de trabajo sin haberlos agregado al índice. Y finalmente --hard hubiera deshecho todo y estaríamos de vuelta en el primer paso (antes de agregar cambios al directorio de trabajo).

Ejemplo:

Para este ejemplo vamos a asumir el siguiente historial del git:



Si han seguido los ejemplos, deberían tener el mismo historial que yo (nuevamente, con la excepción de los identificadores).

Nuestro primer ejemplo será imitar lo que ha hecho git commit --amend. Es decir queremos cambiar el mensaje de nuestro último commit. Para esto vamos a usar el modo --soft de git reset:



Y como podemos ver, nos ha quitado nuestro commit 811439d y ha movido master a 0ba7347. No solo eso, pero en nuestro directorio de trabajo y en el índice tenemos la copía de la última versión, por lo que ahora simplemente necesitamos hacer git commit. Como el objetivo de este ejercicio es cambiar el nombre del commit hare git commit con un mensaje diferente:



Y así, hemos emitado el comportamiento de git commit --amend.

Digamos ahora, que no quiero conservar nada de este último commit. Esta vez usare el modo --hard:



Ahora ni el índice, ni el directorio de trabajo tiene rastro de los cambios que hice y master nuevamente ha cambiado de posición.

Ahora contemplemos el caso en el que no hayamos agregado un .gitignore, hemos agregado un archivo sensible al índice junto con otros archivos importantes y está vez hemos hecho commit. ¿Como podemos arreglarlo? Es decir, ¿Como podemos conservar los archivos importantes o significativos y deshacernos solo del archivo problema? Para esto utilizare el modo --mixed.

Primero, creare el archivo importante.txt con el contenido 'muy importante'. También creare un archivo password.txt con el contenido micontraseña. Agregare ambos archivos al índice y hare commit de ellos.



Por fortuna, me he dado cuenta que el archivo existe en mi último commit así que puedo substituir el último commit con git reset. Como --mixed es el modo por defecto, no necesito especificarlo.



Y ahora solo necesito agregar el archivo que necesito:



El archivo password.txt sigue en mi directorio de trabajo al crear el nuevo commit y git status seguirá molestando hasta que agregue un .gitignore. Realmente no importa mucho equivocarse entre --soft y --mixed. Corregir el estado del índice es sencillo la mayoría de las veces. Pude también haber usado --soft por ejemplo y simplemente remover el password.txt del índice con git rm --cached password.txt.

Ahora, digamos que no estoy contento con ningún commit y quiero volver al commit inicial. Hare git reset al primer commit:



Aquí es donde la mayoría de la gente se equivoca con el estado resultante del directorio de trabajo (con git reset sobre commits que no son continguos). Muchos esperarían que el directorio de trabajo sea exactamente los contenidos del segundo commit. Es decir, esperán que al agregar estos archivos al índice podremos, hacer commit y obtener exactamente el segundo commit. Sin embargo, la copia del directorio del trabajo (y el índice si se ha usado --soft) corresponde al estado del índice/directorio de trabajo cuando se hizo git reset. En este caso, el estado fue exactamente una copia exacta del último commit (y no del segundo commit).

Estamos en una posición en la que podemos agregar todos estos archivos al índice y obtener el estado que teníamos en el último commit antes de hacer git reset. Para ser más especifico, si hiciera git commit tendría básicamente los mismos cambios entre los 3 commits que ya no forman parte de la rama master (sin los cambios intermedios) en un solo commit. A esto generalmente se le conoce como un squash. En español esto se traduce a aplastar. Es decir, estamos aplastando una serie de commits en uno solo.



Pero, ¿Que si me he equivocado y quiero regresar a mi estado original antes del aquel git reset que "elimino" mis commits? Y es importante mencionar aquí que al igual que git commit --amend, git reset no elimina commits, solo desplaza las ramas de manera que son accesibles normalmente. Y puedo usar este mismo comando para regresar la rama a su lugar en el que estaba. Para esto necesito saber el identificador del commit al que quiero regresar mi rama. ¿Pero que si no puedo encontrar este identificador?

Por suerte para nosotros, git mantiene un registro de donde ha estado cada rama. Incluso hay registros para ver donde ha estado HEAD. El comando para ver estos registros es:

Código (bash) [Seleccionar]
git reflog
#Lease git-ref-log y no git-re-flog




Y aquí tenemos una lista de commits en los que master ha estado. Notamos que la primera linea nos dice la posición en la que está ahora mismo y la operación que provoco que llegaramos a este commit. La segunda linea dice que hemos hecho un git reset y acabamos en de00cee (aquí la descripción ha duplicado el identificador, pero los identificadores usualmente salen al principio de la linea). La tercera linea dice que hemos creado un commit. Esta es la posición que buscamos (antes del reset) y corresponde a 5a76bed.

Así que podremos hacer:

Código (bash) [Seleccionar]

git reset 5a76bed




Y no hemos perdido nada. Hemos usado --mixed lo que significa que solo hemos conservado nuestro directorio de trabajo. El otro commit no ha sido eliminado, así que podemos revisarlo con git checkout sobre el commit o volver a desplazar la rama ahí.

Notación especial para especificar commits

Hasta ahora, hemos usado el identificador SHA-1 de cada uno de los commits para referirnos a esos commits. En algunas ocasiones hemos usado master y HEAD, los cuales son mucho más sencillos de usar que el identificador. Existen otras formas de poder referirnos a estos commits. Por ejemplo HEAD^ se refiere ala primer padre de HEAD. HEAD incluso puede ser escrito como @ para simplificar aún más. En nuestro reflog, podemos ver que tenemos una notación: master{n}, la cual nos entrega la posición de master n cambios atrás.

Hay un número de notaciones especiales para poder especificar el commit que necesitamos. No he utilizado estás notaciones en los ejemplos porque quizás puedan tener problemas con su shell (en mi shell, necesito escapar ^ por ejemplo), así que he usado los identificadores SHA-1.

Epilogo

Al finalizar esta parte de la guía. Deben poder hacer la mayoría de las cosas necesarías en git con su repositorio local.

el-brujo

guau, buena lectura. Excelente. Muchas gracias por la aportación ;-) ;-) ;-)

Aunque creo que primero debo leerme y entender la introducción, la primera parte  :P

Introducción a Git (Primera Parte)
https://foro.elhacker.net/programacion_general/introduccion_a_git_primera_parte-t507860.0.html


MinusFour

Si creen que hay algún error o no entienden alguna parte del texto por favor pregunten :D

Le he dado varias lecturas rápidas ahora y he corregido unos cuantos errores (una imagen no salía bien por ejemplo).