elhacker.net cabecera Bienvenido(a), Visitante. Por favor Ingresar o Registrarse
¿Perdiste tu email de activación?.

 

 


Tema destacado: Arreglado, de nuevo, el registro del warzone (wargame) de EHN


  Mostrar Temas
Páginas: [1] 2 3 4 5 6 7
1  Programación / Desarrollo Web / MOVIDO: ¿Cual es la diferencia entre un proxy y un tunel de red? en: 24 Enero 2021, 20:18 pm
El tema ha sido movido a Redes.

https://foro.elhacker.net/index.php?topic=508731.0
2  Programación / Programación General / Repositorios remotos y flujos de colaboración (cuarta parte) en: 23 Enero 2021, 18:48 pm
Prefacio

En este tema veremos como trabajar con repositorios remotos y estrategias para poder colaborar con otras personas usando  estos repositorios remotos. Es una continuación de este tema.

Como recordatorio, esta es una guía informal a git.

Repositorios remotos

Un repositorio remoto es básicamente cualquier otro repositorio que no sea ese mismo repositorio. Un repositorio B es considerado un remoto de A así como A puede ser considerado un repositorio remoto de B. En pocas palabras, el que sea remoto simplemente nos indica una relación entre estos repositorios. Dependiendo de los permisos de acceso al repositorio remoto, uno podrá agregar u obtener información sobre el repositorio.

git necesita poder acceder al repositorio remoto de alguna manera. Los métodos de acceso varían, podemos acceder al repositorio remoto a través de nuestro sistema de archivos, usando SSH, HTTP(S) o el protocolo de git. Cada uno tiene sus ventajas y desventajas. Los métodos de acceso más populares hoy en día son SSH y HTTPS. Para efectos de esta guía usaremos nuestro sistema de archivos y sus permisos correspondientes en Linux porque explicar como montar tanto los servidores SSH como HTTPS merecen su propia guía aparte.

Creación de un repositorio remoto

Antes de empezar repasemos el estado actual del repositorio en nuestro sistema de archivos:

Código:
code
└── proyecto
    ├── barfoo.txt
    ├── bar.txt
    ├── baz.txt
    ├── foobar.txt
    ├── foobaz.txt
    ├── foo.txt
    ├── .git
    ├── importante.txt
    ├── prueba.txt
    └── README.md

Dentro de mi carpeta code tengo la carpeta proyecto que alberga nuestro repositorio. Mi carpeta code está dentro de mi HOME (~). En este ejercicio queremos crear una copia de nuestro repositorio en proyecto a una carpeta que también residirá en code (una carpeta al mismo nivel que proyecto).

Para esto, podemos simplemente copiar nuestro repositorio. Desde una interfaz gráfica es simplemente copiar y pegar. En la terminal podemos usar cp. Sin embargo, también tenemos otra alternativa: git clone.

El comando, es bastante sencillo:

Código
  1. git clone proyecto proyecto-clon

Esto nos entregaría una copia exacta de nuestro repositorio dentro de la carpeta proyecto-clon y añadiría el repositorio proyecto como un repositorio remoto de proyecto-clon.

Sin embargo, hay algunas desventajas de realizar esta operación. Si bien es cierto que puedes interactuar con cualquier repositorio externo, realizar operaciones sobre el repositorio como está puede resultar desastroso. Tanto proyecto como proyecto-clon contienen un directorio de trabajo. Realizar operaciones de escritura sobre la misma rama tanto de proyecto a proyecto-clon puede resultar en un estado ambiguo.

Para esto tenemos los repositorios "simples". Un repositorio simple no contiene un directorio de trabajo, simplemente contiene la estructura del directorio .git. Para crear un repositorio simple podemos hacer:

Código
  1. git init --bare

Y si tenemos un repositorio existente podemos también clonarlo con git clone y convertirlo a un repositorio simple:

Código
  1. git clone --bare proyecto proyecto-simple

En este caso, voy a crear un nuevo repositorio vacío pero será un repositorio simple.



Este repositorio no contiene ningún dato de momento. Lo que haremos ahora es agregar un nuevo remoto a nuestro repositorio en la carpeta proyecto. Para esto vamos a usar los comandos bajo git remote.

El comando para agregar un remoto es:

Código
  1. git remote add nombre url

Por convención, el remoto con el cual interactuamos más lo llamamos origin (usualmente un repositorio central). Pero esta es una convención que no está enforzada por git y le podemos dar cualquier otro nombre. Para este ejemplo yo le daré el nombre de origin.



Podemos listar los repositorios remotos con el comando:

Código
  1. git remote

Y si queremos ver mas información acerca de los remotos podemos usar:

Código
  1. git remote -v



Enviando información al repositorio remoto

Nuestro interés es tener una copia de nuestro repositorio en este nuevo repositorio remoto así que tenemos que enviar nuestros commits a este repositorio. Para hacer esto tenemos que usar el comando git push.

El comando se usa así:

Código
  1. git push repositorio refspec

Donde respositorio es el nombre dado al repositorio remoto y refspec es lo que queremos actualizar/agregar.

refspect sigue un formato en especifico, fuente:destino. Donde fuente puede ser cualquier objeto o rama, mientras que el destino tiene que ser una referencia (rama o etiqueta). Se puede especificar solo la fuente y git buscará que referencia actualizar. De modo que podemos especificar solo master, git entenderá que quieres hacer master:master. Para ser más específicos el refspec completo sería refs/heads/master:refs/heads/master pero git te permite abreviarlo. La ventaja de usar un refspec completo es que podríamos especificar otros lugares fuera de los convencionales para almacenar información (por ejemplo, una llave pública GPG). Pero este ya es un caso más avanzado y realmente no es un caso de uso habitual.

Por ahora, me interesa copiar solo master de mi repositorio en proyecto a proyecto-simple.



git ha comprimido nuestros objetos y los ha enviado a nuestro otro repositorio. Al final nos dice la cantidad de objetos agregados y también nos dice que hemos creado una nueva rama master sobre este repositorio.

Podemos verificarlo en el otro repositorio:



Regresamos al repositorio original y revisemos nuestras ramas que tenemos.



En esta ocasión he usado el argumento -a para mostrar todas las ramas existentes. Y podemos ver las ramas que habíamos creado anteriormente, la rama en la que estamos ahora mismo en verde y una nueva rama que no habíamos visto antes en rojo, remotes/origin/master.

Esta rama es considerada como una rama remota. Esta rama mantiene el estado de una rama en el repositorio remoto. Para mostrar solo las ramas remotas podemos usar:

Código
  1. git branch -r

Cuando hacemos uso de git log este también nos dice en rojo acerca de la rama remota.



Aquí podemos ver que la rama master del repositorio origin esta apuntando al mismo commit que nuestra rama local master que también es HEAD (la rama en la que estamos trabajando). En pocas palabras, nuestras ramas están al corriente.

Rastreando ramas remotas

Algo que podemos hacer también es configurar nuestras ramas para que rastreen el estado de una rama en otro repositorio remoto. Esto tiene la ventaja de que git status y git branch nos pueden decir más acerca del estado de nuestras ramas con respecto a sus contrapartes en los repositorios remotos.

El comando para hacer esto es:

Código
  1. git branch -u remoto/rama rama

Donde remoto/rama es el nombre de la rama remota y la rama es la rama local.

Vamos a decirle a git que rastree los cambios en la rama remota origin/master sobre nuestra rama local master:



Ahora, si usamos git status:



Tenemos una nueva linea que nos dice que nuestra rama está al corriente con la rama que estamos rastreando. Cuando hagamos un commit nuevo sobre nuestra rama local este nos dirá que estamos adelante de la rama remota por un commit. Cuando el repositorio remoto este adelante de nuestra rama local nos dirá que nuestra rama local está atrasada por un número de commits.

Generalmente, este comando no es utilizado mucho. La razón de esto es porque normalmente configuramos está opción cuando creamos la rama. En este caso, cuando hicimos git push este creo la rama remota pero no le hemos indicado como rastrear la nueva rama que creamos. Es preferible utilizar el argumento -u a la hora de hacer push sobre una rama nueva.

Código
  1. git push -u repositorio refspec

De esta manera podemos configurar la rama que hemos enviado para que rastree la nueva rama creada por git push. No habrá necesidad de usar git branch para configurar la rama.

Recibiendo información de un repositorio remoto

No solo es importante poder enviar nuestra información sino también recibir información de otros posibles repositorios.

Para este ejemplo, vamos a clonar nuestro repositorio simple. Es muy común utilizar un repositorio simple como punto de colaboración entre repositorios. Es por eso que estamos clonando del repositorio simple, porque es lo más habitual. Más acerca de esto adelante.



Aquí he clonado el repositorio de la carpeta proyecto-simple a una nueva carpeta proyecto-clon. Este es un repositorio con un directorio de trabajo y al hacer git clone también ha agregado el repositorio remoto origin el cual apunta a proyecto-simple. También ha configurado la rama master para que rastree los cambios sobre origin/master.

En pocas palabras ha hecho prácticamente todo lo que nosotros hicimos en nuestra sección anterior. De modo que este repositorio es muy parecido al que tenemos en la carpeta proyecto. Con la excepción de que nuestro repositorio en proyecto contiene otras ramas que no hemos enviado a proyecto-simple. Esas ramas fueron previstas para ser locales y no tenemos intención de publicarlas a proyecto-simple. No son importantes pero es importante hacer la distinción del estado de estos dos repositorios.

En la imagen anterior también podemos ver una nueva rama remota, origin/HEAD. Es muy parecido al HEAD en nuestro repositorio excepto que los repositorios simples no tienen un directorio de trabajo, así que HEAD tiene otro uso para ellos. origin/HEAD representa la rama por defecto que utiliza un repositorio después de clonar. Al momento de clonar el repositorio simple, nuestro HEAD toma el valor de origin/HEAD. En pocas palabras, la rama que observamos por primera vez después de clonar es la estipulada por origin/HEAD. Esto es útil si la rama principal con la que trabajamos no es master. Existen formas de actualizar origin/HEAD de forma que personas que usen git clone sobre el repositorio tendrán esta nueva rama como la primera rama que ven.

Por lo pronto, lo que queremos hacer es hacer un commit desde nuestro clon y enviarlo a nuestro repositorio simple.



Aquí he agregado un commit con el mensaje: "Agrega archivo barbaz". Nuestro git log nos dice que nuestro master (que también es HEAD) está un commit adelante de origin/master y origin/HEAD. Nuestro git status nos lo repite mucho más explicitamente: Your branch is ahead of 'origin/master' by 1 commit.

Ahora lo enviare al repositorio simple:



Y ahora el commit está en nuestro repositorio simple pero no en el repositorio original en la carpeta proyecto. Lo que hare ahora es cambiarme al repositorio original y revisare el estado de mi repositorio.



Mi repositorio dice que está a la par con origin/master pero en mis registros no aparece el commit que agrega barbaz. ¿Que es lo que está pasando? Pues git no ha revisado el estado de la rama remota. La última vez que git reviso esa la rama en el repositorio remoto fue antes de que hiciera el commit. Necesito revisar nuevamente por posibles cambios en el repositorio remoto. Para esto utilizare git fetch.

Código
  1. git fetch repositorio refspec

El refspec es realmente lo mismo que con git push. Excepto que en está ocasión los roles están inversos. Recordamos que el formato del refspec es fuente:destino. En esta ocasión fuente es la rama del repositorio remoto que queremos obtener y destino es una rama remota local adicional que podemos crear. Personalmente, yo nunca utilizo un refspec completo aquí. Lo único que tenemos que decirle es que nos entregue una rama en especifico.



Aquí git fetch a obtenido el commit y git status nos dice que la rama master está detrás de origin/master por un commit. Es por eso que origin/master no aparece en el git log ya que este está mostrando todos los commits en master y el commit que está en origin/master no forma parte de la rama todavía. Si queremos ver las ramas remotas en nuestro git log podemos usar el argumento --remotes:



También podríamos simplemente agregado origin/master al final del comando.

Ahora, como he mencionado anteriormente, el nuevo commit no forma parte de la rama master dentro del repositorio en la carpeta proyecto. Tenemos dos ramas diferentes, ¿Cómo puedo incluir los cambios de la rama origin/master sobre la rama master? Pues eso es muy fácil, con git merge.



Y ha hecho un "Fast Forward Merge" lo que significa que solo ha actualizado la rama master. Para incluir los cambios también podemos usar git rebase pero en este caso, la tarea era para git merge.

Finalmente tenemos git pull.

Código
  1. git pull repositorio refspec

El comando es muy similar a git fetch. git pull haria lo mismo que git fetch solo que también incluirá los cambios sobre la rama en la que estamos trabajando. Es así de sencillo. Es un comando que lo hace todo, obtiene los cambios del repositorio remoto y los incluye en nuestra rama. Tampoco es muy habitual usar un refspec completo aquí pero lo menciono para ser completo.

Código
  1. git pull origin master

Haría git fetch origin master seguido por git merge origin/master sobre la rama master.

También tenemos la opción de usar:

Código
  1. git pull --rebase origin master

El cual hace un rebase antes de hacer el merge.

Crear y eliminar una rama en el repositorio remoto

Bueno, ya sabemos como crear una rama porque lo hemos visto anteriormente pero si aún no hemos caido en cuenta de como hacerlo:

Código
  1. git push repositorio rama

La rama tiene que existir localmente así que tendríamos que crearla primero si no existe:

Código
  1. git branch rama

Para eliminar la rama en el repositorio remoto:

Código
  1. git push --delete repositorio rama

Si alguién ha borrado la rama remota desde otro repositorio y seguimos teniendo la rama remota (e.g. origin/master) podemos borrar la rama remota con:

Código
  1. git branch -d -r repositorio/rama

Probablemente quieras eliminar la rama local si buscas remover la rama remota, así que como recordatorio:

Código
  1. git branch -d rama

Una nota acerca del estado de los repositorios

Por lo general, la manera en la que actualizamos una rama remota es publicando descendientes directos del commit al cual apunta la rama remota. Esto quiere decir que si en nuestro repositorio simple tenemos:

Código:
A -> B -> C -> D -> E (master)

Y en nuestro repositorio local tenemos:

Código:
A -> B -> C -> D -> F (master)

git no nos dejara actualizar la rama remota porque F no es un descendiente directo de E sino de D.

¿Porqué ocurriría esto? Podría ser por varias razones. Por ejemplo, podría ser que el commit E fue hecho por otro usuario y subido al repositorio simple antes de que pudieramos haber hecho git push con el commit F. Podría ser también que nosotros teníamos E en nuestro repositorio local pero hemos movido la cabeza de master a D y posteriormente creado nuevos commits sobre master.

La regla de oro de git es no reescribir la historia de una rama publicada. Esto quiere decir que si E ha sido publicado en la rama master cualquier cambio sobre master debe incluir a E también, para siempre. Publicar nuestros commits através de git push debe considerarse como una acción irreversible. Si has públicado un commit, este commit debería vivir en este repositorio público para siempre.

La razón de esto es muy sencillo, si alguien empezará a trabajar con la rama públicada y añadiera unos commits:

Código:
A -> B -> C -> D -> E -> G -> H -> I

G, H e I no serían parte de nuestra rama. Porque nosotros divergimos de D. La única forma de rescatar estos otros commits sería hacer un git rebase de la rama master en el repositorio remoto sobre nuestra rama master en nuestro repositorio local.

Ahora imagina que hay otros usuarios que también tienen estos commits en su rama y que también han añadido más commits. Tendrías que esperar a que ese usuario publique sus commits porque el seguiría trabajando con alguno de los commits publicados. Recordemos que git rebase no elimina commits, sino que escribe nuevos commits. Imagina ahora que pasaría si hubiera 5 o más usuarios trabajando con multiples commits.

Sin embargo, existe la opción para forzar a actualizar la cabeza de una rama a un commit que no es decedendiente directo de la punta actual. El argumento que se usa es --force.

Código
  1. git push --force repositorio refspec

En mi opinión esta es una herramienta que no debería utilizarse muy a menudo pero existen casos legitimos para usar --force. En general, --force debe usarse con mucho cuidado. Si la rama que queremos reescribir es una rama pública, cersiorarse de que nadie más este trabajando sobre ella en ese momento. Si alguien está trabajando sobre ella notificarle que la rama va a cambiar y que es probable que tenga que actualizar su repositorio.

Los culpables más comúnes de porque necesitamos usar --force:

git rebase: El comando tiene que buscar un commit en común entre las dos ramas y generalmente implica retroceder en la historia de la rama. Esto significa que la nueva rama no incluirá un número de commits que pudieron haber sido publicados anteriormente. La rama que se busca desplazar no debería recrear commits que ya han sido publicados anteriormente.

git reset: Si bien, git reset puede desplazar las cabezas hacía adelante, su caso más común es desplazar las cabezas de las ramas hacía un punto atrás en el historial. La regla es no hacer git reset sobre una rama de forma que commits publicados ya no están en su procedencia.

git amend: El comando revierte un commit desplazando la cabeza de la rama a su padre por lo que los siguientes commits creados ya no forman parte de la procedencia del commit al cual aplicamos el git ammend. La regla es no utilizar git amend sobre un commit publicado.

Como última advertencia: No busques "cambiar" commits públicos en los cuales otras personas pueden estar trabajando. No existe tal cosa como "cambiar" commits, los tienes que recrear. Si has llegado a usar --force debio haber sido por una muy buena razón (y por lo general, no es esta).

Resumiendo los comandos

git fetch: Obtiene los cambios nuevos de repositorios remotos.

git pull: Obtiene los cambios nuevos de repositorios remotos y los incluye en nuestras ramas locales.

git push: Envia nuestros cambios a otros repositorios remotos.

git clone: Clona nuestros repositorios y configura los clones para trabajar con los repositorios fuente (el repositorio a clonar) como repositorio remotos.

El argumento --bare para git clone y git init para crear un repositorio en su forma simple (sin directorio de trabajo).

El argumento -u en git push y git branch para configurar las ramas remotas que las ramas locales deben rastrear por cambios.

El argumento --force en git push nos permite enviar nuestras ramas y forzarlas a actualizarlas si es necesario (cuando los nuevos commits no sean ancestros directos del último commit en la rama).

Flujos de Colaboración

A continuación voy a mencionar las practicas mas comúnes (en mi opinión) para colaborar con varios individuos usando git.

Equipos/Proyectos Pequeños

Repo Central una sola rama

La forma más común (y más sencilla) para trabajar en proyectos pequeños consiste en mantener un repositorio central (un repositorio simple con --bare) en el cual todos los miembros involucrados tienen permisos de lectura y de escritura. Todos los cambios terminan integrados en una sola rama a través de todos los repositorios (cada usuario tiene su repositorio local). Su objetivo es el de tener un historial lineal.




Flujo de trabajo ideal para este escenario:

Un usuario nunca hace git push sin antes haber hecho git pull. Esto quiere decir que una vez que el usuario está listo para enviar su información, necesita primero revisar que nadie haya publicado nada en el repositorio remoto. No hay problema si alguien intenta hacer git push sin antes hacer git pull porque git avisará que no hay forma de incluir los cambios. git advertirá que la única forma posible de hacerlo es através del argumento --force. Es importante NO USAR --force en este caso sino hacer git pull. En esta situación también se recomienda utilizar git pull --rebase pero dependerá del estado de la rama remota.

En general, cuando vamos a enviar un nuevo commit al repositorio central nos encontraremos con dos situaciones. Puede ser que nadie haya subido nada al repositorio central y nuestro commit se envia sin más o puede ser que alguien haya puesto uno o varios commits en el repositorio central.

Si alguien ha compartido más commits desde la última vez que revisamos la rama remota esto significa que nuestro trabajo diverge del trabajo de los demás. Básicamente tenemos está situación:


Ambas ramas pueden tener uno o más commits divergentes, pero solo he dibujado un commit para ejemplificar la divergencia.

Este es el resultado de hacer git fetch sobre un repositorio remoto y la rama tiene un commit que diverge. Lo normal después de hacer un git fetch es hacer git merge (otra vez, esto es lo que hace git pull practicamente).

En esta situación git merge hará un "Three Way Merge" y creara un "merge commit". En pocas palabras esto quiere decir que para incorporar nuestros cambios tengo que crear un nuevo commit.


Después de esto podemos hacer git push y estaríamos enviando no solo el commit con nuestros cambios, sino también el merge commit. Al hacer esto tanto master como origin/master se volverían el mismo.

Ahora imagina que el usuario que puso código divergente tiene código que todavía no publica al repositorio central mientras que tu ya has publicado tu código con el merge commit. El repositorio del usuario antes de hacer el git pull:


Y después de hacer el git pull (git fetch y git merge):


Y si tu usuario ha hecho un commit antes de que el otro usuario haya hecho git push al repositorio.


Muy bonito para crear un helice DNA en 2D pero esto es git. Este es el problema de los merge commits. Creamos un commit extra, tenemos que unir ramas y al final no tenemos ni idea del historial. Se explico brevemente en el tema anterior pero vuelvo a repetirlo porque es de extrema importancia entender que herramientas utilizar para evitar tener miles de merge commits inútiles.

Entonces, ¿Que podemos hacer?

Volvamos al punto inicial en el que recogimos la información del repositorio remoto con git fetch.


Ahora si en lugar de hacer un git merge hacemos un git rebase:


En lugar de crear un merge commit aquí se ha desplazado el commit divergente. Como se explico git rebase en el tema anterior, este ha guardado el commit, ha cambiado la rama master para que esta y origin/master apunten al mismo commit y finalmente ha aplicado de nuevo el commit que ha guardado sobre la rama master. El resultado es como se aprecia en la gráfica. Hacer push ahora es muy simple.

El segundo usuario que tenía un commit sin públicar tendría un estado así si hubiera hecho git fetch en lugar de git pull:


Y después hecho un git rebase:


Y si tu usuario hubiese creado un commit antes de que el otro usuario haya publicado el suyo y haces git fetch:


Y finalmente git rebase:


Y como pueden ver, nuestro historial ahora es lineal y no está plagado de merge commits. La rama por consecuencia se puede leer fácilmente. No hace falta hacer git fetch seguido por git rebase. He puesto ese ejemplo para que puedan visualizar como se ven las ramas remotas en cada paso. Pueden utilizar git pull --rebase que usara git rebase en lugar de git merge.

Así que el flujo para publicar info es bastante sencillo:

Código
  1. git pull --rebase origin master
  2. git push origin master

Si han configurado su rama master para que rastree la rama remota origin/master entonces esto es todavía más sencillo:

Código
  1. git pull --rebase
  2. git push

Inclusive pueden configurar la rama para que siempre haga git rebase:

Código
  1. git config branch.master.rebase true

Y ahora pueden hacer:

Código
  1. git pull
  2. git push

Y si solo quieren incorporar los cambios de otros en sus repositorios locales:

Código
  1. git pull

Este es de lejos, la manera mas sencilla de trabajar. Prácticamente no hay ninguna otra rama fuera de master y lo único que tienen que hacer es git commit, git pull y git push. Si han clonado el repositorio ni siquiera tienen que configurar las ramas remotas ni los repositorios remotos.

Pero tiene sus desventajas. En primer lugar, la rama master se vuelve practicamente un entorno de trabajo en los que todos ponen su trabajo. Imagina que esto es el equivalente de N número de personas trabajando sobre un mismo escritorio y sobre los mismos documentos que estás trabajando. Tu quieres hacer algo, la otra persona quiere hacer otra cosa distinta, uno está deshaciendo lo que el otro hace y el otro deshaciendo lo que tu haces. Se necesita coordinar con el equipo para poder trabajar correctamente vaya. Quizás no haya tanto problema con dos personas o tres pero con grupos arriba de cinco se pueden poner las cosas díficiles.

Todos los individuos también tienen acceso de escritura sobre el repositorio para colocar sus cambios. Esto significa que por cada usuario que tenga acceso al repositorio se incrementa la posibilidad de equivocarse y provocar daños al repositorio. Imagina que uno de los usuarios se le ocurre subir sus cambios con un montón de commits que no hacen nada. Por ejemplo, modifica un archivo y hace commit. Se da cuenta que ese es un cambio malo, así que modifica de nuevo el archivo, lo deja en su estado inicial y vuelve hacer commit. Ahora hace git push y ha dejado 2 commits que practicamente no hacen nada. Este es un caso ligero pero habrá algún usuario que se le ocurra subir un desastre de historial, como el que habíamos comentado anteriormente. Inclusive pueden haber usuarios que hagan git push --force y no avisen a los demás usuarios.

Repo Central una sola rama por usuario

Este es un flujo de trabajo un poco más elaborado que personalmente no es de mi agrado. En este flujo de trabajo existe una rama master en la que eventualmente se termina incluyendo todos los cambios justo como en el ejemplo anterior. La gran diferencia es que también existe una rama para cada usuario diferente en el repositorio central. De esta manera cada usuario publica sus cambios sobre una misma rama. No hay necesidad de hacer git pull sobre esta rama si queremos enviar nuestros cambios porque en teoría solo el usuario puede escribir sobre esta rama. Así que solo es cuestión de hacer git push.

Cada usuario prepara su rama apartir de master:

Código
  1. git checkout -b miusuario master

Y realiza su trabajo en esta rama. Una vez que el usuario haya alcanzado una meta (termino una nueva abilidad del programa o similar) el usuario puede realizar un par de cosas:

1) Solicitar a los demas usuarios permiso para incorporar sus cambios sobre la rama master.
2) Incorporar sus cambios sin pedir permiso.

Para la primera opción usualmente se realiza una acción conocida como "Pull Request" (PR). Un PR es simplemente una petición para incorporar los cambios en una determinada rama (y en un determinado repositorio) sobre una rama en el repositorio. En este caso, el usuario solicitaría que se incluyera los cambios en la rama miusuario sobre la rama master.

Através del PR los demas miembros del proyecto pueden leer los cambios propuestos por el usuario y dar sus opiniones antes de incluir los cambios. Los PRs son practicamente externos a git (aunque git permite enviar parches por email) y dependerá de la accesibilidad de los cambios que los otros usuarios tengan. En esta ocasión, los demás usuarios pueden ver los cambios de dicho usuario publicado sobre la rama de este.

Para revisar la rama de otro usuario por primera vez:

Código
  1. git fetch origin miusuario
  2. git checkout miusuario

Una vez que hayamos terminado de revisar la rama y queremos incluir los cambios:

Código
  1. git checkout master
  2. # Si la rama master no esta actualizada
  3. # git pull
  4. git merge miusuario
  5. git push

Es importante que la rama master este al corriente tambien:

Si queremos actualizar la rama miusuario (para volver a revisar cambios) más adelante:

Código
  1. git checkout miusuario
  2. git pull

Los usuarios siguen usando sus ramas y cada vez que llegan a un punto en el desarrollo incluyen los cambios en la rama master. Su historial se puede ver así:


La ventaja es que cada usuario tiene su propia rama en la cual no tiene que estar coordinando con otros usuarios. Todo mundo sabe donde estan los cambios de todos los demas usuarios. También abrimos la posibilidad de poder revisar los cambios antes de que acaben integrados con los demas.

La desventaja de este flujo de trabajo es que su historial no es el de todo simple de seguir. Eventualmente los usuarios necesitarán incorporar los cambios de master dentro de sus ramas, así que todos los usuarios necesitan hacer git merge master desde sus ramas. Si los cambios en la rama no son los adecuados, es muy probable también necesiten usar git revert para crear commits que eliminen los otros commits o si es posible hacer git reset y git force --push.

Personalmente, yo no usaría este flujo de trabajo pero el siguiente es muy parecido y corrige la mayoría de las fallas.

Repo Central una sola rama por meta

Este flujo de trabajo es muy similar al anterior, la única diferencia es que en lugar de tener una rama por cada usuario, se crea una rama con el fín de realizar una acción en especifico y justo después que la rama se integra con master la rama no se útiliza nunca más.

Se crea la rama en la cual trabajar:

Código
  1. git checkout -b meta1 master

Una vez que llegamos a la meta en nuestra rama subimos el código al repositorio:

Código
  1. git push origin meta1

Y de aquí es cuando podemos evaluar los cambios o no.

Para revisar la rama se realizan los mismos procedimientos:

Código
  1. git fetch origin meta1
  2. git checkout meta1

Si se acuerda que los cambios deben ser integrados entonces algún usuario necesita obtener la rama e integrarla a master:

Código
  1. git checkout master
  2. # Si la rama master está desactualizada
  3. # git pull
  4. git merge meta1
  5. git push

La única diferencia aquí es que una vez que la rama haya sido integrada, no la volveremos a usar. Si queremos seguir trabajando en el repositorio tenemos que crear una nueva rama (empezando de master).

Entonces si el mismo usuario quiere seguir colaborando necesita hacer:

Código
  1. git checkout master
  2. git pull
  3. git checkout -b meta2

Ahora, tenemos algunas posibilidades aquí para manejara el historial de nuestro repositorio. Por un lado podemos tener un historial lineal, sin bifurcaciones. Esto es muy sencillo. Si las ramas empiezan y terminan una después de la otra cada merge que hagamos hará un "Fast-Forward Merge".

Es decir, si tenemos esto:


Hacer git merge haría esto:


Las dos ramas aquí apuntan al mismo commit. Si la siguiente rama divergiera de este punto el siguiente git merge tambien haría un "Fast Foward Merge". Sin embargo, lo que estamos diciendo aquí es que solo una rama podría salir de master lo que significa que solo se podría trabajar sobre una sola acción/meta hasta que esta termine para empezar la siguiente. Esto no es práctico. Lo ideal es poder trabajar en paralelo.

Digamos entonces que dos ramas diverge de master en el mismo commit. Si integro una de estas ramas, la siguiente rama ya no puede ser integrada con git merge si quiero tener un historial lineal. ¿Que podemos hacer? git rebase claro. Los pasos a seguir serian estos:

Código
  1. git rebase master meta2
  2. git checkout master
  3. git merge meta2
  4. git push

Es importante tener esta rama meta2 dentro de nuestro repositorio (y al corriente). Nuestra rama se moverá hacia adelante. Y así se integra cada una de las ramas que no divergan del último commit en master. De aquí podrías eliminar las ramas o la alternativa es actualizar las ramas con git push --force o borrar las ramas remotas primero. Nadie más debería tocar esta rama de aquí en adelante.

El resultado de esto sería un historial lineal.

Ahora, los historiales lineales son fáciles de leer pero no siempre es fácil encontrar donde termino el desarrollo de una rama y donde empieza el desarrollo de la siguiente rama. Aquí es donde los merge commits entran al rescate.

Las operaciones son exactamente las mismas, excepto que al hacer git merge usaremos el argumento --no-ff.

Código
  1. git merge --no-ff meta2

Esto siempre creara un merge commit. Lo que significa que siempre podemos ver donde una rama empieza y donde termina.

Digamos que tenemos un historial así:


Y digamos que nuestros compañeros han dado el visto bueno a las dos ramas. Ahora tenemos que integrarlas dentro de master.

Si integro primero la rama meta1 y después la rama meta2 sin hacer git rebase y sin usar --no-ff:


Si integro ambras ramas con --no-ff:


Y finalmente si las integro con --no-ff y git rebase:


La última opción es la que ya mencione la cual es usar git rebase solamente (historial lineal).

En mi opinión --no-ff es indispensable para este flujo de trabajo. git rebase no es estrictamente necesario a menos que la rama tenga una base muy antigua, la comprensión del historial no se ve muy afectada por una o dos ramas con la misma base.

Equipos/Proyectos Medianos-Grandes

Repositorios Intermedios

Este sigue siendo un modelo en el que hay un repositorio central. La única diferencia aquí es que uno no estaría push sobre el repositorio central. Existen variaciones pero la idea es prácticamente la misma.


Este es un flujo de trabajo muy común. La idea es que existe un repositorio central en el cual se integra todo el código. Por lo general, solo una persona se encarga de integrar el código de otros repositorios y esta persona puede o no utilizar este repositorio como su repositorio para publicar su código u optar por tener un repositorio separado (no está en la imagen).

Esto quiere decir que los miembros del proyecto tienen su propio repositorio "público" y uno local. Los demás miembros tienen permisos de lectura a estos repositorios de manera que uno no puede publicar nada en estos repositorios pero si pueden obtener cambios publicados. La excepción es generalmente el individuo que se encarga de publicar al repositorio central. Su función es de administrador básicamente y tener acceso de escritura a los repositorios de los otros miembros le permitiría manejar cambios necesarios antes de integrar el código.

Una vez que un usuario alcanza una meta en el proyecto tiene que solicitar permiso al integrador para que su código forme parte del código central. El proceso por lo general involucra a los otros miembros del equipo y estos también pueden decir que cambios son necesarios o aceptables. Una vez que se determina la validez de los cambios propuestos, el integrador debe tomar los cambios e incluirlos en una rama publicada en el repositorio central.

Su uso normal es generalmente así:

1. El usuario, que se encarga de mantener el repositorio central, publica su repositorio en algún lado para que todos tengan acceso de lectura.
2. Cada usuario crea una copia del repositorio central y también publican este repositorio de manera que los demás también tengan acceso de lectura. Acceso de escritura para el usuario que integra los cambios es opcional pero recomendado.
3. Cada usuario clona la copia del repositorio central.
4. Cada usuario trabaja sobre su repositorio local.
5. Cuando un usuario da por terminado su trabajo, lo hace publico a través de la copia del repositorio central.
6. El usuario notifica al usuario integrador que sus cambios están listos para integrarse.
7. El usuario integrador descarga los cambios de la copia del repositorio central y los publica en una rama de su repositorio central.
8. El usuario descarga los cambios realizados sobre el repositorio central y los publica en la copia de su repositorio público (eventualmente).

Un proceso un tanto elaborado pero también muy sencillo. El verdadero problema de esto es encontrar un lugar donde poner tantos repositorios. Aquí es cuando la gente acude a servicios como Bitbucket, Github o Gitlab. Es muy probable que ya hayas escuchado de estas copias de los repositorios bajo otro nombre: "Forks" y estos servicios son reconocidos por preferir este modelo bajo "forks".

Ahora, para un determinado usuario, su proceso no es tan diferente como uno pudiera pensar.

Mi recomendación es tener dos repositorios remotos configurados. Por convención, "origin" es nuestra copia del repositorio (de aquí en adelante le llamare fork). Cuando hacemos git clone sobre enuestro fork, el remoto de origin se configura automaticamente. El segundo remoto que debemos agregar es el repositorio central. Al cual generalmente llamo upstream:

Código
  1. git remote add upstream urlalrepositorioremoto

Ahora, este flujo de trabajo favorece drásticamente al flujo de trabajo anterior. Se podría decir que este es todavía un paso extra sobre el flujo de trabajo anterior pero no es exactamente necesario.

Empezaremos trabajando sobre una rama con su punto de origen en master:

Código
  1. git checkout -b meta1 master

Una vez terminado con nuestra rama haremos push:

Código
  1. git push -u origin meta1

Es posible continuar trabajando sobre esta rama una vez publicada. Sin embargo, al momento de avisar al usuario integrador que el trabajo esta completo sería ideal no tocar la rama más o avisar al usuario integrador que el trabajo está incompleto.

Una vez que los miembros del proyecto y el usuario integrador validen los cambios le tocara al usuario integrador recoger estos datos de tu "fork" e integrarlos. Los servicios como Github, Bitbucket y Gitlab hacen está tarea trivial puesto que solo necesitan hacer uno o dos clicks para integrar tu código en una rama.

En el caso de que usuario integrador quiera usar la terminal este tendrá que agregar tu fork como repositorio remoto de su copia en local.

Código
  1. git remote add user1 urlaforkdeusuario1

Una vez agregado el repositorio remoto tendrá que obtener los cambios de la rama en especifico.

Código
  1. git fetch user1 meta1

Seguido por el git merge:

Código
  1. git checkout master
  2. git merge user1/meta1

Para finalmente colocar los cambios en el repositorio central con:

Código
  1. git push origin master
  2. # origin para el usuario integrador es el repositorio central

Las mismas estrategias para mantener el historial aplican aquí. El usuario integrador puede usar git rebase antes de hacer el git merge:

Código
  1. git checkout -b meta1 user1/meta1
  2. git rebase master

Y por supuesto usar el argumento --no-ff para git merge:

Código
  1. git merge --no-ff meta1
  2. # Sin rebase no necesitamos una rama local podemos usar la rama remota
  3. # git merge --no-ff user1/meta1

El usuario finalmente necesita obtener los cambios publicados:

Código
  1. git fetch upstream master
  2. git checkout master
  3. git merge upstream/master

Y también borrar las ramas locales y remotas que ya no son necesarias:

Código
  1. git push --delete origin meta1
  2. git branch -d meta1

Y el ciclo se repite ad infinitum.

Las ventajas de tener varios repositorios en los que actuán como repositorios de lectura para otros es que los usuarios pueden equivocarse sobre estos repositorios. Si alguién ha publicado código que no esta listo no afecta a los demás miembros del equipo porque los demás miembros del equipo basan su trabajo sobre el repositorio central no sobre el repositorio de un determinado miembro. Los demás miembros y el usuario integrador pueden decirle al usuario que necesita corregir el historial o el código y lo haría sobre fork.

En el peor de los casos en el que el usuario ha hecho daños en el cual ya no puede reparar su fork, lo único que el usuario tiene que hacer es eliminar por completo su fork y crear un nuevo fork del repositorio central. Una opción que no es posible si se trabaja directamente sobre el repositorio central.

Ramas auxiliares

Por lo general los cambios se integran sobre una rama (hasta ahora solo hemos usado master) pero también es muy común tener otras ramas para diferentes propósitos. El caso más común es tener una rama de desarrollo y otra de producción.

Por ejemplo, los cambios de los miembros del equipo se integran en la rama de desarrollo y una vez que el proyecto este listo para distribuirse la rama de desarrollo se integra en la rama master.

Un ejemplo de como se vería el historial:


Las ramas meta1 y meta2 son ramas en las que los usuarios trabajan. El usuario integrador las integra en la rama develop. Una vez que los cambios en develop sean suficientes para distribuirse devleop se integra en master.

El flujo de trabajo que popularizó multiples ramas auxiliares fue GitFlow. Quizás para proyectos más robustos sea necesario tener todas estas ramas auxiliares pero yo creo que para la gran mayoría de proyectos 1 o 2 ramas son suficientes.

La ventaja de trabajar en esta forma es que podemos realizar ciertas acciones cuando un commit llega a cualquiera de estas ramas. El ejemplo más común es desplegar un entorno de desarrollo cuando la rama se integra a la rama de desarrollo, correr un número de pruebas, etc. En la rama master podemos desplegar código inmediatamente sobre el entorno de producción. Realizar acciones en especifico merece su propia guía realmente por eso solo lo mencionare.

Epílogo

Esta es la última parte de lo que yo consideraría indispensable para cualquier persona que maneje git. El único tema faltante a considerar es como publicar nuestros repositorios y hacerlos públicos.

La siguiente parte (lo estoy considerando todavía) sería enseñar como usar un servicio como Github para realizar algunas operaciones administrativas sobre los repositorios publicos.
3  Programación / Programación General / Trabajando con las ramas de git (tercera parte) en: 14 Diciembre 2020, 18:30 pm
Prefacio

En este tema se explora como trabajar con las ramas de git. Es una continuación de este tema.

Como recordatorio, esta es una guía informal a git.

Las ramas en git

Como habíamos dicho en un principio, las ramas para git son simplemente apuntadores los cuales contienen identificadores de commit. Esto nos permite poder obtener fácilmente una procedencia de commits a la cual normalmente llamamos un historial. Es decir, cada commit que puede ser alcanzado por una determinada rama introduce un determinado número de cambios en la que culminan con el último commit de la rama, la última entrada de nuestro historial.

De esta forma podríamos decir que multiples ramas nos podrían permitir trabajar con diferentes conjunto de cambios sobre un mismo repositorio. Por lo general, no tratamos con conjuntos de cambios exclusivos entre cada rama lo que quiere decir que las ramas por lo general tienen un tronco en común aunque también es posible tener ramas completamente diferentes entre ellas.

Cuando creamos un repositorio en git y empezamos a crear commits, lo hacemos sobre una rama por defecto cuyo nombre es master (lo cual está sujeto a cambiar a main). Sin una rama en la cual apunte a un determinado commit no podríamos tener un historial adecuado. Sería tener un montón de commits en los cuales no tendríamos idea cual es nuestro estado actual. Si bien es cierto que cada commit contiene en sí puede reproducir un historial ya que tiene cada uno tiene una línea de procedencia, no podríamos saber con exactitud si este commit es la punta actual de una rama. Sin mencionar que git se encarga de eliminar commits que no pueden ser alcanzados por una rama. Ultimadamente, son nuestras ramas las que dan forma a nuestro historial.

Resulta entonces indispensable tener por lo menos una sola rama. ¿Pero porque necesitaríamos trabajar con más de una rama? A lo cual yo respondería que no es exactamente imposible trabajar con una sola rama, pero en mi opinión estaríamos obviando una de las ventajas más importantes de git. Es extremadamente común que un proyecto opte por avanzar de diferentes maneras y mantenga diferentes estados por diferentes razones. Cada rama extra en sí representa una nueva posibilidad para avanzar el desarrollo del repositorio. Como desarrollador, escritor, diseñador, animador, arquitecto... ¿Cuantas formas hay de realizar un determinado trabajo? Muchas formas. Estaríamos limitándonos si no entretuviéramos la idea de que quizás podríamos hacer las cosas de manera distinta. Las ramas entonces nos resultan convenientes para poder explorar las alternativas.

Pero estoy adelantándome un poco. La función de está parte de la guía es en sí como manipular las ramas en git, no exactamente como usarlas. Podría enumerar multiples escenarios en las cuales usar multiples ramas es útil, pero creo que sería mejor sí enseño y ejemplifico los usos que yo les he dado.

Listando las ramas

Antes de empezar, revisaremos el estado de nuestro repositorio ejemplo:



Y podemos ver aquí, que tengo un archivo sin rastrear pendiente por agregar al indice: password.txt. En nuestro directorio de trabajo también encontramos los archivos introducidos por 4 commits, un archivo de prueba, un archivo importante y un README el cual contiene el nombre de mi proyecto. Podemos ver también que estamos en la rama master. Por ahora eliminare el archivo password.txt ya que no me interesa.

Código
  1. rm -f password.txt

Ahora lo primero que tenemos que saber acerca de las ramas es... como listarlas. Para esto utilizaremos el comando:

Código
  1. git branch

El cual lista las ramas locales (más adelante veremos los otros tipos de ramas).



git branch colorea la rama en la que estamos de color verde y la marca con un asterisco. Si requerimos más información podemos usar el argumento -v, el cual nos índica el último commit al que apunta (entre otras cosas que veremos adelante).



Creando una nueva rama

Supongamos que quiero agregar nuevos cambios a mi repositorio. Tengo una nueva idea que necesito implementar en mi repositorio y no estoy seguro que vaya a funcionar. Podría trabajar sobre la rama master y si no me gusta lo que he hecho podría simplemente usar git reset para regresar mi rama a su lugar. Tendría que anotar en algún lado el commit al cual quiero regresar la rama o mirar en el log el commit al cual quiero regresar. O podría crear una rama extra para trabajar.

Para crear una rama simplemente usare:

Código
  1. git branch ramanueva



Aquí he agregado una nueva rama llamada: cambios-importantes. git branch me dice que la rama existe y está apuntando al mismo commit que master (5a76bed en mi caso). Podemos ver que la rama actual en la que estoy es master como lo índica git branch.

Cambiando a la nueva rama

Necesito usar la rama cambios-importantes,¿como hago el cambio de ramas? Recordemos que la rama actual en la que estamos esta dada por HEAD y necesitamos mover HEAD a que use está nueva rama. ¿Que comando podemos usar?

Código
  1. git checkout rama/commit

¿Recuerdan este comando? Lo utilizamos para inspeccionar commits pero está vez lo utilizaremos para hacer el cambio a la nueva rama.



Y ahora hemos cambiado de rama y estamos en cambios-importantes. Si revisamos con git status y con git branch:



Podemos ver que ahora ambos reflejan el cambio. git log --oneline también ha cambiado el texto un poco:



HEAD ahora apunta a cambios-importantes aunque también podemos ver que dice master a un lado. ¿Que significa esto? Que tanto cambios-importantes como master están apuntando a este commit. Solo que en está ocasión HEAD -> ya no usa master. git log nos está ayudando a encontrar la posición de nuestras ramas, como lo hace git branch -v.

Trabajando sobre la nueva rama

Bien, ahora lo que hare es crear una serie de commits sobre esta nueva rama. Para ejemplos de esta demonstración introduce cambios arbitrarios sobre archivos foo.txt, bar.txt y baz.txt y cada uno tendrá su propio commit.



Creo que los comandos que he usado se deben de poder entender fácilmente, ya que son los mismos comandos que he usado, solo que los he "juntado" con && para ahorrar espacio. Es un poco difícil ver los cambios como están mostrados por git log. Así que usare una gráfica.

Este es el estado que teníamos antes de hacer los commits. En el momento en que he agregado la nueva rama:


Cuando hicimos git checkout sobre la nueva rama:


Y cuando agregamos los tres commits:


Y aquí tenemos dos puntos marcados por las ramas. En un punto tenemos los nuevos archivos agregados, foo.txt, bar.txt, baz.txt y en el otro no. Podemos olvidarnos completamente de estos cambios haciendo:

Código
  1. git checkout master

Y si queremos volver a nuestros cambios:

Código
  1. git checkout cambios-importantes

Usando multiples ramas para mantener multiples versiones

Supongamos que no estoy contento con el trabajo que he hecho en la otra rama y quiero hacer cambios diferentes. Volveré a la rama master donde están nuestros archivos antes de estos cambios:



Y ahora creare otra rama a la cual llamare, cambios-importantes-2. En está ocasión, utilizare otro comando para crear la rama. La rama anterior ha sido creada llamando a git branch directamente. Posteriormente cambie de la rama master a la nueva rama usando git checkout. He usado dos comandos para crear y cambiar a la rama. git nos permite hacer las dos cosas al mismo tiempo porque es un caso de uso muy común y el comando que se usa es... git checkout con el argumento -b

Código
  1. git checkout -b ramanueva



Me he creado la rama y ha hecho el cambio. Ahora, verificamos el estado de todas nuestras ramas:



Nuestra nueva rama está apuntando al mismo commit que master y HEAD está apuntando a la nueva rama. Bien, digamos ahora que mi solución es crear solo dos archivos en lugar de tres: foobar.txt y baz.txt



Y aquí podemos ver los nuevos commits creados. Nuestro git log nos muestra todos los commits de los cuales podemos acceder desde cualquier rama en nuestro repositorio pero realmente el listado no nos dice mucho. git log nos está mostrando nuestros commits en orden cronológico inverso (últimos primero) pero esto realmente no nos ayuda a mentalizar nuestra línea de procedencia. Para esto, git log tiene un argumento que nos puede ayudar a visualizar nuestro historial mejor:

Código
  1. git log --graph

Lo útilizaremos en conjunto con --oneline (para abreviar) y --all para mostrar todos los commits.



git log ahora nos da una mejor representación de las ramas que tenemos, podemos ver que de master las dos ramas están divergiendo. Tenemos la punta de cambios importantes en 0a4e950 y la punta de cambios-importantes-2 en 77c64b1. Quizás no es del todo claro el formato que está dando git log así que también creare una gráfica:


Digamos ahora que no estoy convencido con ninguna de estas dos ramas y quiero crear una tercera rama. En las dos últimas ocasiones, hemos estados situados sobre la rama master antes de crear la rama ya que tanto git checkout como git branch utilizán HEAD para indicar donde es que la nueva rama debe apuntar. Sin embargo, las dos herramientas nos permiten especificar el commit o rama al cual queremos que nuestra nueva rama apunte. En esta ocasión, quiero crear una nueva rama cambios-importantes-3 que empiece en master (así fue con las otras dos ramas) y quiero cambiar inmediatamente a está nueva rama también. Usare:

Código
  1. git checkout -b nuevarama puntodepartida



Aquí creamos nuestra nueva rama cambios-importantes-3, empieza desde master y hemos hecho el cambio a esta rama también. Aquí verificamos otra vez el estado de nuestro repositorio:



Y en está rama nos interesa tener dos archivos: foobaz.txt y bar.txt. Así que creare los commits respectivos:



git log por desgracia es un poco difícil de leer porque las bifurcaciones no las imprime en paralelo y no hay una sola columna para cada rama. Si tienen problema visualizando, las trazare encima para que las puedan ver mejor:



Pero también utilizaré una gráfica nuevamente para mostrar el estado de nuestro repositorio:


Obteniendo las diferencias de cada rama con respecto a una rama en común

Ahora, revisaremos el estado de cada uno de las ramas con el comando git diff:

Código
  1. git diff rama/commit rama/commit

Donde la primera rama/commit es el punto inicial y la segunda rama/commit es el punto final. El comando como lo indica, nos regresa las diferencias entre dos commits. El orden es importante. Supongamos que A y B son dos commits que marcan dos estados diferentes de nuestro código. Dentro de A no existe C.txt pero si existe dentro de B. Si usamos git diff B A, nos dirá que la diferencia es que se ha borrado C.txt. En cambio, si usamos git diff A B nos dirá que se ha agregado C.txt.

Lo probaremos sobre la rama cambios-importantes:



Y aquí nos muestra que la diferencia entre nuestra rama master y cambios-importantes es que cambios-importantes ha creado 3 archivos, bar.txt, baz.txt, foo.txt.

No es necesario que los commits sean parientes para poder hacer git diff. También podría usar git diff entre las diferentes ramas que hemos creado:



Por ejemplo, en este caso, cambios-importantes-2 no tiene foo.txt ni bar.txt pero si tiene foobar.txt.

Las ramas que hemos creado no son excepcionalmente diferentes, cada uno agrega archivos diferentes. Usare el argumento --name-status para solo imprimir los archivos que fueron agregados.



Conozco muy bien los cambios en cada archivo así que no es necesario hacer una inspección completa.

Eliminando una rama

Digamos ahora que no me ha gustado para nada los cambios en cambios-importantes-3. ¿Como puedo deshacerme de esa rama? Para eso usamos el comando:

Código
  1. git branch -d rama-a-borrar



Para nuestra sorpresa, git no nos ha dejado eliminar la rama porque estamos usándola. Tendremos que cambiar de rama primero y en está ocasión simplemente ire a master:



Y nuevamente, git no nos ha dejado borrar la rama! En esta ocasión git nos advierte que la rama no está integrada desde la rama en la que estamos trabajando. Desde el punto de vista de git los commits que hemos creado en esa rama serían inalcanzables desde nuestra rama actual. En pocas palabras, existe la posibilidad de perder una forma de acceder a los commits si borramos esta rama. Eso es exactamente lo que queremos así que vamos a forzar el borrado de la rama (con la opción -D[/t]):



Recuerden que los commits que no son alcanzables por una rama son eventualmente borrados pero hasta entonces, todavía siguen existiendo. Podríamos recrear esta rama nuevamente con el comando:

Código
  1. git branch rama commit

Integrando cambios de una rama a otra

Y no perderíamos absolutamente nada. Ahora he reducido mis opciones a dos posibles ramas, digamos que en este caso me interesan los cambios en cambios-importantes. Podría continuar trabajando sobre esta rama pero el propósito de la rama fue la de introducir un número de cambios y no usar esta rama como la principal. Quiero conservar mi rama tmaster como la rama principal (veremos más adelante el uso de una rama principal). Así que necesito integrar los cambios hechos sobre la rama cambios-importantes e incluirlos en mi rama principal master.

Para simplificar el proceso, primero observemos como se ve nuestra rama master.


Y nosotros queremos incluir los cambios que están en cambios-importantes. Es decir, queremos obtener los cambios introducidos por los commits en cambios-importantes:


Esto es un trabajo para git merge.

Código
  1. git merge rama

Veamos que es lo que hace:



Fast Foward Merge

Lo primero que vemos es que dice que está actualizando 5a76bed contra 0a4e950, ambos son los commits para sus ramas correspondiente. En este caso, hicimos git merge en la rama master (es la rama en la cual estamos, HEAD) y está tratando de actualizar los cambios provenientes de cambios-importantes. La segunda línea nos dice que ha hecho un Fast-Forward y enseguida nos muestra un resumen de los cambios. La segunda línea resulta muy importante porque nos ha dicho que ha hecho un "Fast-Forward Merge". "Fast-Forward" en español se traduce literalmente a "Avance Rápido". Sus reproductores multimedia tienen una función similar de la cual git se ha inspirado para nombrar, el botón que realiza esta función es ⏩.

En este caso no estamos trabajando con un archivo multimedia, sino con una rama. Estamos "avanzando" o "adelantando" la rama. git simplemente ha movido la rama hacia "adelante". Si revisamos nuestro historial con git log podremos ver que nuestra rama master ahora está en la misma posición que cambios-importantes.



Y si queremos visualizar como git ha avanzado nuestra rama, podemos usar una gráfica:


El resultado es entonces que nuestra rama master ahora incluye los commits de cambios-importantes. Está es quizás la manera más sencilla de incluir los cambios de otra rama puesto que solo hemos movido la rama (algunos inclusive dirían que no es un merge verdadero). Podría volver a trabajar sobre la rama cambios-importantes y volver hacer git merge sobre esta, el resultado volvería a ser el mismo. Esto es porque master seguiría siendo un ancestro de cambios-importantes. Dicho de otra manera, cambios-importantes es un descendiente de master. ¿Pero que pasaría si la rama que quiero incluir no es un descendiente de nuestra rama?

3-Way Merge

Tenemos una rama que es así de momento, cambios-importantes-2. La punta de está rama no es descendiente de ninguna de las otras ramas pero ambas si tienen un ancestro en común:



Y usando la gráfica:


Como podemos ver tanto en el git log como en la gráfica, no hay forma de llegar de master o cambios-importantes a cambios-importantes-2. Digamos que quiero incluir estos cambios en master. ¿Que es lo que ocurriría?

En esta ocasión usamos el nombre de la otra rama para git merge:



Y ahora nos salta nuestro editor de texto pidiéndonos un mensaje para un commit. Trágicamente, no sabemos que demonios está pasando aquí.



Por lo pronto, dejamos el mensaje por defecto del commit, guardamos y cerramos. Y ahora nuestra terminal nos dice más:



Y la primera línea nos dice que ha utilizado la estrategia recursiva y que ha agregado un archivo foobar.txt. ¿Pero porque nos ha pedido un mensaje para un commit? Revisemos el historial nuevamente con git log:



En la gráfica:


Y esto es interesante porque aquí podemos ver un nuevo commit que no aparecía antes: 577fe28 con el mensaje que hemos puesto antes en nuestro editor. Lo que es más, master ahora apunta a este commit. ¿Que es lo que realmente ha pasado?

Esto es lo que se conoce como un '3-Way Merge' que es simplemente una manera de decir que se han usado tres puntos de referencia para crear un nuevo conjunto de cambios  que incluye los cambios entre dos puntos. Un nombre en español apropiado quizás sería "Unión basado en tres puntos" pero a lo largo de esta guía seguire usando el termino "3 Way merge" ¿Porque usa un tercer punto de referencia si nosotros solo le hemos pedido que incluya una rama dentro de otra (2 puntos)?

La manera más sencilla de entender porque usa un tercer punto de referencia, es tratar de usar solo estos dos puntos de referencia. Haremos un git diff entre cambios-importantes y cambios-importantes-2 para ver cuales son las diferencias entre las dos ramas:



Podemos ver que foo.txt ni bar.txt no aparecen en cambios-importantes-2 y foobar.txt no aparece en cambios-importantes:


Ahora, observando SOLO estás diferencias quisiera preguntarte: ¿cambios-importantes-2 agrega un archivo foobar.txt o cambios-importantes borra un archivo foobar.txt? La respuesta es... que no lo sabrías si solo tuvieras estos puntos de comparación. ¿Pero si tuvieras un tercero?


Ahora sí puedo deducir quien ha hecho que cambios. El tercer commit es un punto de referencia que se usa como base. Es comúnmente el ancestro común más cercano (la antigua posición de master). La estrategia que usamos tiene algo que decir cuando seleccionamos nuestro ancestro común. En este determinado caso, el ancestro común es bastante claro pero en otras situaciones quizás no tanto. La estrategia que git usa por defecto es la recursiva, la cual hace un "3-Way Merge" con los dos ancestros para usar esta como punto de referencia base.

Como podemos ver, git a producido un conjunto de cambios que no aparecen en ningún commit, así que git no puede simplemente mover la rama, tiene que crear un nuevo commit. Este commit contendrá todos los cambios y el mensaje que nos ha pedido git es para este nuevo commit. Este nuevo commit será algo diferente de nuestros otros commits, tendrá dos padres en lugar de uno. A este tipo de commits generalmente nos referimos por "merge commits" y para muchos, su peor pesadilla.

Estábamos en la rama master que era idéntica a cambios-importantes y nosotros buscábamos incluir los cambios de cambios-importantes-2 sobre master así que los cambios introducidos son básicamente la diferencia sobre el resultado y cambios-importantes (foobar.txt es agregado). Está es la explicación de porque obtuvimos esos cambios específicos en la terminal.

Deshaciendo un merge

Sigamos explorando git merge. Para esto, voy a regresar a master a donde estaba antes de hacer el último merge. Para esto simplemente podemos revisar git reflog sobre master. Yo se que master@{0} es el commit en el que estamos ahora mismo, master@{1} antes de hacer el último merge.

Volvamos entonces a este punto con git reset --hard:



Y básicamente he deshecho el merge. Todo merge puede ser deshecho con git reset puesto que al apuntar la rama a donde estaba podemos deshacer los cambios. Como punto adicional pude haber deshecho los dos merges si hubiese usado hecho git reset a master@{2}. La razón por la cual hago uso de --hard es porque el commit que git crea hace cambios sobre nuestro directorio de trabajo para hacer el commit, recuerden que agrego el archivo foobar.txt. Si hubiese usado --soft o --mixed hubiese conservado los cambios en el directorio de trabajo que hizo git para preparar el merge commit.

Resolviendo conflictos

Ahora, quiero hacer un cambio sobre cambios-importantes-2, primero hago un checkout a la rama:



Quiero introducir otros cambios sobre 'baz', así que hare git reset sobre el último commit (cambios-importantes-2~ es otra forma de referirnos al commit padre de cambios-importantes-2).



Recreare el archivo desde 0 y volveré a la rama master



Ahora podemos hacer nuestro merge nuevamente:



Y ahora nos dice que git merge ha fallado. ¿Que es lo que ha ocurrido? Revisemos el diff entre cambios-importantes y cambios-importantes-2:



Para visualizar mejor las diferencias usare la tabla anterior:


No ha cambiado mucho, seguimos usando la misma base de la cual hacemos comparaciones. Pero ahora baz.txt tiene dos cambios diferentes. En cambios-importantest (y por ende master) tenemos "baz" dentro de baz.txt pero en cambios-importantes-2 tenemos "foo" en baz.txt. Nuestras dos ramas contienen cambios diferentes y git no sabe si conservar los cambios de una rama o de la otra. Esto es lo que se conoce como un merge conflict. Revisemos git log y git status.



Nuestro git log nos muestra que no hemos movido master pero git status nos dice que tenemos "unmerged paths". Tenemos un nuevo archivo agregado al indice (que también está en el directorio de trabajo) pero tenemos una nueva sección que dice "unmerged paths" y dice que "ambos" han agregado baz.txt. ¿Como es esto posible? Si yo agrego un archivo al índice este debería remplazar el anterior. Revisemos a detalle el estado del índice:



Y aquí aparecen dos copias de baz.txt, cada uno con su objeto diferente y un número 2 y 3. La primera pregunta de la mayoría probablemente sea y ¿Porque no aparece 1? La razón de esto es que durante un conflicto git mantiene 3 copias del archivo el cual contiene un conflicto. Mantiene la copia de la base bajo el número 1 pero como no existe el archivo baz sobre nuestro ancestro común (son archivos agregados en ambas ramas) no existe está copia. También mantiene la copia de la rama en la que estamos trabajando y otra copia de la rama que estamos incluyendo.

¿Pero como lo solucionamos? Tenemos que agregar el archivo en conflicto nuevamente al índice. Pero primero exploremos el contenido de nuestro archivo baz.txt:



Nos ha remplazado el contenido de nuestro archivo por lo que se conoce como un marcador de conflicto. git ha explorado ambas versiones y ha puesto los cambios de ambas ramas en el mismo archivo (usando separadores textuales). De forma que nosotros podemos elegir cual de los dos cambios conservar. Es decir, git necesita que nosotros le indiquemos cual de los cambios conservar. No hace falta que sea una de las dos opciones, git simplemente nos informa los cambios de ambas ramas pero nosotros podemos remplazar el marcado de conflicto por cualquier otra cosa. En este caso, voy a optar por conservar los cambios de mi rama. Así que simplemente pondré "baz":



No lo he agregado al índice todavía pero observen como es que git status no nos menciona nada acerca de los nuevos cambios en nuestro directorio de trabajo. La razón de esto es muy sencillo, a pesar de que hice cambios sobre el archivo en el directorio de trabajo. Este archivo es exactamente el mismo que tenemos en nuestro último commit por eso no está detectando ningún cambio. Agreguemos el archivo al índice y revisemos nuevamente.



Ahora solo aparece un solo baz.txt, git status nos dice que hemos resuelto los conflictos pero que todavía estamos en medio del merge. Nos dice que usemos git commit para finalizar el merge.



Hacemos git commit para finalizar el merge. Nos debe abrir el editor de texto para introducir el mensaje del commit. Y listo! Hemos incluido la rama cambios-importantes-2 dentro de master.

Opciones para git merge

Antes de movernos al siguiente tema, mencionare unas opciones importantes con git merge.

Primero, tenemos el argumento --ff-only.

Código
  1. git merge rama --ff-only

El cual NUNCA intenta hacer un "3-Way Merge" y solo nos permite hacer un 'Fast-Forward Merge", fallará si no puede.

Código
  1. git merge rama --no-ff

Lo contrario a --ff-only, esto SIEMPRE crea un "merge commit" usando un "3-Way Merge".

Código
  1. git merge rama --no-edit

Evitamos tener que escribir un mensaje y usamos el mensaje por defecto.

Código
  1. git merge rama -m "Mensaje de commit"

Esto es bastante obvio, nos permite establecer el mensaje del commit sin usar nuestro editor.

Lo caótico que pueden ser los merge commits

Es muy probable que en algún momento tengan que integrar código de otras ramas muchas veces y no siempre será posible hacer un "Fast-Forward Merge". Este es un problema muy común colaborando con otros individuos dentro de un proyecto pero veremos más acerca de esto en la siguiente parte de la guía. Por ahora, volvamos a nuestro ejemplo. Visualicemos una vez más el estado de nuestro repositorio:


Los caminos no han cambiado realmente, lo único diferente es que tenemos dos commits diferentes puesto que los he cambiado para demostrar el último ejemplo.  Supongamos que ahora sigo trabajando sobre master y hago un commit:



Y ahora vuelvo a la rama cambios-importantes porque digamos que quiero agregar unos nuevos archivos y no los quiero en master aún porque no estoy seguro que los quiero incluir en master. Estoy usando cambios-importantest solo para ejemplificar la situación pero normalmente esto no es lo común. Es cierto que pude haber empezado una nueva rama desde el último commit en master (y esto es lo recomendable).



Nuestro historial es ahora un poco más caótico:




Ahora, digamos que quiero incluir los cambios que hay en master para probar si no perjudica en algó lo que tengo en cambios-importantest. Desde cambios-importantest hare un merge:



Revisemos nuevamente el estado de nuestro historial:



Algo difícil de seguir con git log pero ahí está el merge sobre master. La gráfica se está volviendo también más complicada.


Ahora, puedo  volver a master hacer merge sobre la rama cambios-importantes y simplemente movería la cabeza de master a cambios-importantes, en pocas palabras un "Fast-Forward merge". Creo que este historial no es exactamente limpio y tiene potencial para complicarse más y más. ¿Que podemos hacer entonces? Volvamos un paso atrás.




Alterando nuestras ramas para tener un historial más simple

Ahora en lugar de usar git merge para incluir los cambios, vamos a utilizar otra herramienta: git rebase. El uso de git rebase se explica fácilmente, cambiar la base de nuestro commit por otro nuevo. El comando es:

Código
  1. git rebase rama/commit

Básicamente cambiaremos la base de la rama en la cual estamos ejecutando el comando. Entonces, podemos hacer:



El comando nos dice que ha hecho el rebase sin ningún problema y también ha actualizado la rama. Nuestro historial ahora se ve más limpio. La gráfica por si alguien se lo pregunta:


Por favor noten que la rama master sigue apuntando al mismo commit lo que significa que los cambios aún no son parte de master, la rama cambios-importantest es la que ha cambiado. Si hiciera git merge de la rama cambios-importantest desde master este sería un "Fast-Forward Merge" y no habría necesidad de hacer un merge commit.

Habrá alguien que habrá notado que cambios-importantes ahora apunta a otro commit diferente. De e46a69a a db982e0. La razón de esto es que git rebase no ha cambiado el padre del commit (como su nombre sugiere), sino que ha creado un commit exactamente igual reproduciendo los cambios exactamente igual sobre la nueva base (la rama que especificamos, en este caso master).



Lo que git rebase hace realmente es encontrar una basa, un ancestro en común, entre la rama que queremos mover y la rama a la que queremos movernos. Lo siguiente que hace es encontrar las diferencias entre la base y la rama que queremos cambiar. En este caso, la diferencia es un solo commit y ahora querrá aplicar los cambios sobre la rama que le hemos especificado.

git rebase intentará aplicar los commits tal y cual sean la diferencia entre la base y la rama a mover. Sin embargo, git rebase es algo inteligente y no aplicará commits cuyos cambios ya se encuentran dentro de la rama. También detectará si algún commit sobre la rama introduce un conflicto como lo haría git merge en un "3-Way Merge". En ese punto, git rebase se detendrá, te dejará resolver el conflicto y tendrás que decirle a git rebase que continue con:

Código
  1. git rebase --continue

Deshaciendo un rebase

git rebase no elimina commits y podemos deshacer lo que hemos hecho fácilmente con git reset --hard sobre la posición en la que estaba anteriormente.



Incluyendo cambios sin hacer un merge propio

Ahora, en un principio yo quería incluir master dentro de mi rama cambios-importantes y lo que he hecho en mi explicación pasada es incluir cambios-importantes dentro de master que es el objetivo final. Pero en el ejemplo pasado no tuvimos la posibilidad de probar si los cambios en master funcionarían en cambios-importantes.

Podría hacer un git rebase sobre master para obtener este commit y agregarlo mi rama cambios-importantes pero tengo una mejor idea vamos a replicar  este commit en master en cambios-importantest. Para esto utilizare la herramienta git cherry-pick:

Código
  1. git cherry-pick rama/commit

git cherry-pick es una herramienta muy parecida a git rebase porque ambos aplican cambios de un número de commits sobre una rama. La diferencia está en que uno busca "deshacer" commits sobre la rama en la que está para aplicarlos sobre otra rama recipiente sin modificar esta otra. En cambio git cherry-pick busca obtener cambios de otra rama para aplicarlos sobre la rama recipiente y si actualiza está rama mientras que la otra rama permanece igual. Aquí en acción:




He duplicado el commit ce894c3 sobre cambios-importantes y ahora puedo verificar que los cambios no afectan lo que he hecho sobre la rama. Ahora, puedo simplemente revertir la rama antes de este nuevo commit pero mi intención es volver a hacer git rebase:



Ambas ramas incluían el commit "Agrega archivo foobaz", sin embargo al hacer git rebase este no incluyo este commit. La razón es como había explicado, git rebase aplica cambios de manera inteligente y en este caso se ha fijado que los cambios ya estaban en la rama. Así que no los incluye. Una gráfica para ser más específico:


Caso más elaborado para git rebase

git rebase realmente puede hacer mucho más. Es una herramienta muy flexible. Puedo tomar un rango de commits y aplicarlos sobre otro commit. Por ejemplo, digamos que quiero quitar ese merge commit que salió de master y cambios-importantes-2. Tendría que tomar todos estos commits:


Y aplicarlos sobre 0a4e950.

Esto es bastante sencillo de hacer. Primero haré el merge de cambios-importantest ya que ahora master está un commit detrás (el rebase anterior no ha movido master sino cambios-importantes).



Ahora sí, el estado concuerda con nuestra gráfica anterior. Ahora hare el rebase:



Resolviendo un conflicto de git rebase

En un momento explicare el comando, pero quiero mostrarles que en efecto ahora mismo estoy haciendo el rebase pero me ha dado un conflicto. ¿Recuerdan que en nuestro último ejemplo de git merge teníamos un conflicto con baz.txt al hacer git merge? Tuvimos que solucionar el conflicto manualmente y hacer git commit. Ahora git rebase nos está pidiendo exactamente lo mismo. Nosotros no optamos por conservar los cambios de baz.txt que introducía 32eb804 y este es el único cambio que introducía ese commit. Podemos revisar que otros cambios contiene git status:



Aquí es cuando la gente entra en pánico porque git status advierte que estás en medio de un rebase... interactivo?? Si, como he dicho antes git rebase es una herramienta sumamente flexible. Un rebase interactivo no es nada más que un rebase en el cual podemos interactuar entre cada uno de los pasos que realiza. De hecho podemos inclusive modificar o añadir nuevos pasos. Pero nuestra intención no fue la de empezar un rebase interactivo, simplemente nos ha tocado un conflicto y ahora tenemos que arreglarlo así que nos ha puesto en el modo interactivo para poder arreglarlo. Lo importante aquí es que no hay ningún otro cambio fuera de baz.txt. Si hubiese otros cambios tendríamos modificaciones por agregar al índice y no hay ninguna otra mas que el conflicto en baz.txt. El modo interactivo es una historia para otra guía :).

Como este commit no haría nada (porque mi intención es dejar a baz.txt con baz, cambio ya agregado desde la base) voy a saltarme este commit con el comando que me advierte git status:

Código
  1. git rebase --skip

En el caso de que necesitamos resolver un conflicto de manera que produce cambios diferentes a un commit existente tendremos que realizar el proceso común, agregar los cambios al índice y hacer commit o git rebase --continue. Las opciones son muy similares a las que nos ofrece git merge. --continue, --abort, etc. Creo yo que son bastantes intuitivas y no necesitan explicación.



Fuera de ese conflicto... la operación ha sido todo un éxito. Veamos como está nuestro git log ahora:



Nuestra historia ahora es completamente "lineal". No hay bifurcaciones ni nada por el estilo. Si comparamos las dos salidas nos damos cuenta que son los mismos commits con la excepción de dos commits. El merge commit ya no existe porque por defecto git rebase no recrea los merge commits. Teníamos también dos instancias de commits: "Agrega archivo baz" pero yo le he dicho a git rebase que simplemente salte este commit y no lo aplique. Fuera de estos dos commits, los otros commits que existen tienen los mismos cambios aunque sean nuevos commits (solo 3 son nuevos commits).

Como funciona git rebase realmente

Exploremos más a fondo que hace este comando. El primer argumento a git rebase es la rama/commit del cual obtener una base obtener el conjunto de commits a aplicar sobre nuestro objetivo.

Código
  1. git rebase A

Si A es un ancestro directo de HEAD (la rama que estamos revisando) esto significa que la base será A. El conjunto de commits seleccionados incluye todos desde A hasta HEAD pero sin incluir A. En otras palabras si tenemos:

Código:
A <- B <- C <- D <- E

Donde E es HEAD, el conjunto de commits que se selecciona es B, C, D y E. La cosa se complica más cuando se usa un commit que no es un ancestro directo como base. git tendrá que encontrar un ancestro en común entre los dos commits y ese será la nueva base. Los commits a aplicar siguen excluyendo está base pero incluyen todos los demás commits que no son ancestros del otro commit. En mi opinión es mucho más sencillo siempre especificar la base manualmente pero también se lo pueden dejar a git.

El segundo argumento que podemos establecer es la rama a la cual queremos hacer el rebase. Esto implica cambiar a la rama especificada, por ende cambiar HEAD. En mis ejemplos no he puesto la rama master porque HEAD ya es master. Especificar la rama no haría nada más que ser más explícitos con el comando. Recuerden que el conjunto de commits a aplicar se mide desde la base a HEAD. Está rama también será desplazada una vez que los nuevos commits hayan sido agregados sobre el objetivo.

Por defecto, el objetivo al cual aplicar los commits es también el primer argumento. Esto quiere decir que los commits obtenidos que no se encuentran en la línea de procedencia del primer argumento a la base en común de los dos commits/ramas son agregados sobre este primer argumento. En el caso en que la base coincida que también es el primer argumento (porque el primer argumento es ancestro de HEAD) el resultado sería recrear todos los commits en esa línea de procedencia. Si el primer argumento está un commit adelante de la base, en la línea de procedencia de la rama que está siendo movida (HEAD) tendrá la diferencia entre su posición original y la base más ese único commit.

Por fortuna, nosotros podemos especificar un objetivo diferente que la rama/commit que se usa para establecer la base. Para esto se usa la opción: --onto. Esto significa que puedo especificar la base de la cual crear el conjunto de commits a replicar y especificar en donde quiero poner estos commits.

El comando que yo utilice para este último rebase toma como argumento --onto 0a4e950. Esto quiere decir que los commits que quiero replicar los va agregar desde este punto. También le he especificado el argumento 5a76bed. Lo que significa que tomará todos los commits entre 5a76bed y HEAD (master) y los aplicará sobre 0a4e950. Al finalizar, moverá master para que apunte sobre el último commit a replicar.

Cuando hice git rebase las tres primeras líneas empezaban por Dropping.... La razón de esto es sencilla, la base que he seleccionado incluye todos los commits que he señalado anteriormente. Uno de esos commits es un merge commit y ese merge tiene otra línea de procedencia que también es incluida hasta llegar a la base. En pocas palabras, git rebase también ha seleccionado estos otros commits:


Pero el punto de inserción que he marcado (0a4e950) ya tenia incluido esos commits (porque son ancestros directos) así que no los ha recreado. El comando continua recreando los otros commits (que son los que yo necesito recrear) pero se detiene en el commit que introduce el conflicto sobre baz.txt. Al cual yo le he dicho que simplemente no lo recree y continua agregando los otros commits. Por defecto, git rebase no trata de recrear merge commits pero no descarta ningún commit del cual se pueda acceder desde ese commit. Así que lo que ha hecho es juntar los dos "linajes" sobre una misma línea de procedencia.

Finalmente, termina de recrear todos los commits y apunta la rama al último commit recreado. Es importante notar que ninguna de las otras ramas ha sido desplazada a los nuevos commits creados. git rebase no cambiará las ramas a sus contrapartes recreadas, así que necesitarán actualizar estas ramas o simplemente borrarlas y recrearlas.

Para resumir... por si no ha quedado claro, hay tres partes en cualquier rebase. La rama que se está moviendo (apuntada por HEAD), la base del cual se calcula el conjunto de commits diferentes entre HEAD -> base y objetivo -> base. La rama/commit objetivo sobre la cual el conjunto de commits será aplicado para calcular la rama a mover. Por defecto el primer argumento es el objetivo y también se usa para calcular la base. Se puede especificar un objetivo diferente con --onto pero la base siempre se calculara con el primer argumento. El segundo argumento simplemente es una manera corta para no tener que hacer git checkout sobre la rama y/o para ser más explícitos con el comando.

Epílogo

Al terminar está guía deberían poder crear y borrar una rama en cualquier parte de su repositorio. Deben tener una noción básica de como incluir los cambios entre diferentes ramas y como es que git trabaja para incluir esos cambios. También deberían tener una noción básica de como recrear su historial usando el comando git rebase.

Por último, mencionare que hay una tendencia por parte de los usuarios de git a favorecer git rebase sobre git merge y viceversa. Son dos herramientas diferentes que por lo general son utilizadas en conjunto y no se excluyen mutuamente. "git rebase es mejor" o "git merge es mejor" son opiniones erroneas, cada una tiene su uso y es necesario entender cuando sería mejor usar una y cuando usar las otra.
4  Programación / Programación General / Usando Git para manipular el directorio de trabajo, el índice y commits (segunda parte) en: 30 Noviembre 2020, 20:43 pm
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
  1. 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
  1. 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
  1. 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
  1. git checkout rama/commit /path/a/archivo

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

Código
  1. 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
  1. git checkout HEAD -- README.md
  2. git checkout master -- README.md
  3. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. git stash pop

Para aplicar los cambios y conservar el stash simplemente usamos:

Código
  1. git stash apply

Para borrar el stash:

Código
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. git reset rama/commit -- /path/a/archivo/

Lo cual les puede resultar muy familiar al comando:

Código
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. git add -A
  2. git add -u
  3. git add .
  4. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. git rm --cached password.txt
  2. 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
  1. rm -f .gitignore password.txt
  2. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. git reflog
  2. #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
  1. 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.
5  Programación / Programación General / Introducción a Git (Primera Parte) en: 20 Noviembre 2020, 19:42 pm
Prefacio

Este es una breve lectura introductoria a git. El propósito es tener una explicación personal de git para miembros del foro. No es su propósito ser extremadamente específico y explicar git en su totalidad, ni de describir como funciona cada una de las herramientas que lo compone.

He puesto ejemplos que pueden seguir asumiendo que tengan acceso a una terminal y al propio programa de git. Los ejemplos están trabajados sobre un sistema en Linux pero es posible seguir los ejemplos usando Windows. Esta no es una guía para instalar git ya que hay diversas formas en las que uno puede descargar e instalar git. Sin embargo, para aquellos que estén en Windows yo recomiendo que utilicen git tal cual es disponible desde este sitio, ya que el instalador también provee un entorno similar al que uno tiene en un sistema Linux (Git Bash).

Se requiere saber un mínimo acerca de la terminal para seguir los ejercicios. En concreto, uno debería poder navegar el sistema de archivos a través de la terminal.

¿Que es git?

git es un sistema de control de versiones distribuido (DCVS, Distributed Version Control System). Su principal uso es el de mantener el historial del código fuente en un proyecto y poder manejar cada uno de los cambios sobre el código fuente a través del tiempo. Hoy en día, un número de herramientas se han desarrollado a la par de git por lo cual se podría decir que git se utiliza para muchas otras cosas.

Por ejemplo, las plataformas para distribuir código libre usan principalmente git. En un pasado, el código fuente era distribuido por otros sistemas de control de versiones, por ejemplo SVN. En el peor de los casos el código fuente se distribuía en comprimibles para cada versión. Hoy en día, git se ha vuelto el VCS por defecto. De forma que si quieres obtener el código fuente de un proyecto lo más probable es que necesites usar git. De igual forma, si quieres contribuir a un proyecto lo más probable es que necesites usar git también.

git está en todos lados. Administración de proyectos, distribución de paquetes, integración/entrega continua y muchos otros procesos en el desarrollo de software. Es muy probable que si estás involucrado en el desarrollo de algún software tengas que usar git en alguna ocasión. De hecho, como consumidor de software es probable que también tengas que usar git en alguna ocasión. De manera que aprender git nunca está de más.

¿Que significa que git sea distribuido?

Tradicionalmente, el control de versiones se hacia sobre una instancia central (un servidor) en la cual personas (clientes) introducían o pedían código fuente. Todo se procesaba en esta instancia central. Lo que significa que no podías agregar cambios o solicitar código fuente si la instancia no estaba disponible. En un sistema central todos dependen de está instancia.

En un sistema distribuido, uno no depende de una sola instancia. En sí cada cliente es dueño de una instancia que se puede valer por si misma. Así que cada instancia puede procesar cambios y solicitar información independientemente de cualquier otra instancia. Cada instancia puede recibir y compartir cambios entre todas las instancias que existen.

Cada instancia se le conoce como un "repositorio".

¿Que es un repositorio?

Del latín repositorium: Lugar donde se guarda algo.

Un repositorio de git es básicamente un lugar donde se guarda (o almacena) el código fuente. Un repositorio está marcado por la carpeta .git/ en la cual se encuentra toda la información acerca del repositorio. La carpeta que contiene a la carpeta .git/ es conocida como la carpeta raíz del repositorio. La carpeta raíz es la carpeta que contiene una versión del código fuente y también es conocida como directorio de trabajo para git.

¿Como crear un repositorio de git?

Para crear un repositorio en blanco de git se utiliza el comando:

Código
  1. git init

Dentro de esta carpeta se creará la carpeta .git/, la cual contendrá toda la información del repositorio.

Es importante mencionar que el repositorio está vació a estas alturas. La raíz del proyecto (la carpeta donde se hace git init) puede contener información a la hora de crear el repositorio pero está información todavía no forma parte del repositorio. Necesitan ser agregados manualmente.

Ejemplo:

Nota: Para efectos de esta explicación voy a utilizar un sistema en Linux con git.

Supongamos que tenemos nuestro directorio proyecto/:



Hasta ahora, es un directorio normal. El siguiente paso sera crear un archivo README.md que contenga el nombre del proyecto.



Ahora lo convertiremos a un repositorio git.



Y vamos a notar que el mismo comando nos está diciendo que el repositorio está vacío.

¿Como agregar información al repositorio?

Antes de empezar a agregar información al repositorio, es muy importante conocer como es que git mantiene nuestra información.

git mantiene conjuntos de archivos en lo que se denomina un commit. Es uno de las términos de los cuales no quisiera traducir literalmente al español. El commit es básicamente una replica del estado de tu código fuente en un punto en el tiempo. Es decir, el commit contiene el código fuente exactamente igual a la vez que se creo dicho commit. Dentro de un repositorio, pueden existir miles de commits. Cada uno simbolizando el progreso del código a través del tiempo.

¿Como crear un commit? ¿Que es el índice de git?

Para crear un commit, es importante también conocer un poco acerca del índice de git. El índice de git mantiene los cambios a agregar a un commit. Los cambios pueden ser varios entre cambiar el nombre a un archivo, remover o agregar un archivo o hacer alguna modificación sobre el archivo.

Es a través del índice de git en el cual podemos construir nuestros commits. Son los cambios agregados al índice los que forman parte del commit. Puedes pensar del índice como el paso intermedio a la construcción del commit. Imagina que el índice es un contenedor abierto (una caja de cartón por ejemplo) a la cual vas agregando cosas. Una vez que terminas de empaquetar todo, cierras este contenedor y ahora una vez cerrado este contenedor deja de ser el índice y ahora es un commit.

Ejemplo:

Vamos a empaquetar nuestro archivo README.md, es decir vamos a agregar el archivo al índice. Primero vamos a visualizar el estado del índice. Para revisarlo, podemos utilizar:

Código
  1. git status



Como puedes observar en la última línea git me está diciendo que no hay nada para hacer el commit pero hay archivos que no están siendo rastreados. Nuestro índice está vacio.

Usamos el comando que nos menciona git para agregar el archivo al índice:

Código
  1. git add README.md

y volvemos a revisar el estado del índice:



Ahora git dice que hay un cambio en el índice para el cual podemos crear un commit. Por lo pronto vamos a hacer otro cambio a nuestro archivo README.md y simplemente vamos a agregar una descripción.

Lo único que voy a hacer es agregar la siguiente línea a mi archivo, "Este es mi proyecto que estoy haciendo con git". Ustedes pueden usar cualquier editor para agregar la linea si no quieren usar su terminal.



Y como pueden ver, el índice ahora está rastreando los cambios en el archivo README.md. Y el índice me alerta que hay cambios pendientes que no han sido agregados al índice todavía.

Si yo creara un commit en este mismo momento, lo único que añadiría al repositorio son los cambios que han sido agregados al índice hasta ahora. Es decir, el nuevo archivo con el texto original (antes de la modificación).

Para que este nuevo cambio sea parte del commit, necesito agregarlo al índice. Nuevamente tenemos que usar:

Código
  1. git add README.md

Y al revisar el estado nuevamente con git status obtendremos los mismos resultados antes de hacer la modificación.

Finalmente, podemos crear nuestro commit. Para ello necesitamos usar el comando:

Código
  1. git commit


Importante:
Si está es la primera vez que utilizan git va a ser necesario configurar los datos relevantes al autor que realiza el commit. Configurar estas opciones son muy sencillas:

Código
  1. git config --global user.email "tucorreo@dominio.com"
  2. git config --global user.name "Tu nombre"


Un editor deberá aparecer (dependiendo de la configuración de git) y les pedirá que introduzcan un mensaje asociado al commit. Este mensaje en general actúa como un resumen de los cambios que se han realizado en ese dado commit. No es necesario que sea así, el mensaje puede ser cualquier cosa excepto un mensaje vació. Si el mensaje es vació (y no se ha configurado git para aceptar mensajes vacios), git aborta el comando y no se crea ningún commit.

Opcionalmente podemos utilizar:

Código
  1. git commit -m "mensaje corto"

Y esto nos permite crear un mensaje rápido para hacer el commit. Vamos a usar esta opción para crear el commit en caso de que no tengan un editor configurado correctamente.



Más acerca de los mensajes en los commits más adelante. Por ahora este es un ejercicio de prueba y la intención es que puedas agregar tus cambios al repositorio. Una vez que el commit se haya agregado puedes considerar que tu información es parte del repositorio. Si volvemos a checar el estado del índice veremos que no tendremos ningún cambió pendiente a agregar:



Y si queremos revisar que nuestro commit está ahí podemos usar:

Código
  1. git log



En el cual podemos observar varias cosas:

1) El nombre del Autor es mi usuario: "MinusFour"
2) Entre las flechas < > aparece el correo del autor (en mi caso lo he tapado).
3) La fecha en la que se realizo el commit.
4) El mensaje del commit.
5) Un identificador del commit (SHA-1 por defecto): de00cee7484d435d471b964920e6122a1511b788

Y por último, dos palabras entre paréntesis que tienen un significado especial: HEAD y master.  Ambas están relacionadas al siguiente tema importante de Git, las ramas.


Importante:
El identificador SHA1 no será el mismo para ustedes. Cada commit que se realize es único, incluso si son los mismos cambios. git no necesita el identificador entero para funcionar puedes usar solo una parte siempre y cuando no haya otro commit que también comparta esa parte.

¿Que es una rama (branch)?

Hasta ahora sabemos lo que es un repositorio y sus commits. Las ramas son el eslabón faltante entre estos dos. Cada repositorio está compuesto de una o varias ramas que estos a su vez están formados por commits. Las ramas en sí no son nada en especial para git y al mismo tiempo resultan extremadamente útil. Primero tenemos que explorar un poco más a fondo los commits.

Verás, cada commit después del primer commit tendrá un ancestro del cual proviene. Es decir, el primer commit será el ancestro del segundo commit, el segundo será el ancestro del tercero y así sucesivamente. Esta línea de sucesión entre commits es lo que se podría considerar una rama. Y git no necesita mucho para averiguar todos los commits que componen una rama. Usando un solo commit, git puede rastrear hasta el último ancestro (porque cada commit tiene un ancestro). Es por eso que las ramas son esencialmente punteros los cuales contienen el identificador del último commit, también llamado la punta de la rama.

Cuando creamos un repositorio, git crea una rama por defecto bajo el nombre de master (esto nombre posiblemente este por cambiar a main en un futuro). Cuando nosotros creamos un commit sobre la rama master, git remplaza el identificador del commit anterior por el identificador del nuevo commit (ya que este es el último commit, la nueva punta de la rama). ¿Y Como sabe git en que rama estás haciendo el commit? Pues para eso está el puntero especial HEAD cuya función es la de apuntar directamente al commit o rama en la que estamos trabajando. Cuando HEAD no apunta a una rama y apunta a un commit directamente se dice que la cabeza está desconectada (detached head). En esta situación, al hacer un commit git no sabría que rama actualizar para apuntar al nuevo commit recién creado. Es necesario tomar precauciones cuando trabajamos de está forma ya que sin una rama los cambios realizados pueden perderse. En nuestro ejemplo anterior podemos ver que HEAD está apuntando a la rama master. Esto simplemente quiere decir que la rama en la cual estamos trabajando actualmente es master y que cualquier cualquier operación que modifique el estado de la rama (como git commit) será sobre master.

Ejemplo:

Ahora mismo nuestro repositorio solo tiene un commit con el identificador de00cee. Nuestro HEAD está apuntado a master que a su vez está apuntando a de00cee. Todo esto lo podemos verificar con los archivos internos de git.



Aquí podemos ver que HEAD apunta a refs/heads/master (una notación más especifica para master) que a su vez apunta a de00cee. Lo podemos ver de una manera gráfica:



Para ejemplificar como es que la rama empieza a tomar forma vamos a agregar unos cuantos commits.

Nuestro primer commit simplemente cambiara el nombre del proyecto en el archivo README.md. En esta ocasión estoy utilizando el programa sed para cambiar el texto "Mi Proyecto" por "Aprendiendo Git", pero ustedes pueden abrir su editor de texto y hacer cambios sobre el README.md.



Y el tercer commit simplemente agregara un archivo nuevo al cual por ahora simplemente llamaremos prueba.txt.



Ahora podemos revisar todos nuestros commits usando git log nuevamente.



Podemos ver nuestros tres commits: 0ba7347, 0f55ed3 y de00cee. Tambien podemos ver como master ya no está apuntando a de00cee sino al último commit 0ba7347. HEAD sigue apuntando a master.



Ahora, para mostrarles que efectivamente el ancestro/padre de 0ba7347 es 0f55ed3 usare un comando especial para mostrarles el contenido del commit:



Usando esta información podemos crear una nueva gráfica:



Recapitulando el estado de este repositorio de prueba:

1) Tenemos tres commits. Cada uno apunta a un padre (con la expceción del primer commit).
2) La rama master apunta al último commit.
3) Nuestro HEAD apunta a master (lo que significa que la rama sobre la cual estamos trabajando es master.

Más adelante pondré ejemplos de como usar multiples ramas para organizar el repositorio.

Epilogo

Al final de la lectura deberán saber que es git, para que se usa git y deberían poder crear sus propios repositorios y agregar información a ellos. Deberían tener una noción básica de lo que es un repositorio, los commits, el índice de git y las ramas.
6  Foros Generales / Foro Libre / Tremenda paliza que le está metiendo AMD a Intel en: 29 Octubre 2019, 00:12 am
Jamás hubiera pensado que el AMD de hace 3 o 4 años le pudiera hacer lo que le está haciendo a Intel hoy en día. La nueva linea de procesadores CascadeLake-X corta precios a la mitad de lo que te encuentras en SkyLake-X (mismo número de cores/threads) y a decir verdad todavía están un poco lejos de la meta en cuanto a precio y rendimiento.

Hasta ahora no han salido los procesadores CascadeLake-X y en HEDT solo está SkyLake-X ahora mismo. Los precios (MSRP?) son los siguientes:



Fuente Aquí

Yo diría que AMD R9 3900x deja en a prietos a la mayoría de los CPUs SkyLake-X (quizás no al 9980XE) y el precio de este es $530 USD (MSRP), vamos que más de la mitad. Lo gracioso es que en las lineas anteriores (CoffeLake-X) tenían el mismo modelo de precio que SkyLake-X, así que sin lugar a duda estaban más que preparados para cobrar lo mismo con CascadeLake-X. Y bueno, creo que no cabe duda de porque están cortando los precios de está linea.

El R9 3900X muy probablemente vaya a estar a la par con los procesadores de la nueva linea y siendo todavía barato. Y AMD aún está por introducir un nuevo procesador de 16 núcleos (R9 3950x) que es todavía más barato que el i9-10940x (que tiene 14 núcleos) e incluso hay benchmarks entre el R9 3950x y el i9-10980XE en la que AMD está por encima y siendo 30% más barato.

Y lo mejor de todo es que la competencia directa de CascadeLake-X (ThreadRipper Zen 2) está por llegar (el siguiente mes). Y yo creo que podemos esperar precios similares a los de CascadeLake-X. Lo que me hace dudar si Intel todavía piensa cortar precios todavía aún más.

Por supuesto, con la llegada de CascadeLake-X no van a poder dejar los precios así para SkyLake-X, así que los van a cortar también... aunque hasta donde tengo entendido los precios no van a ser tan diferentes de CascadeLake-X.

El siguiente mes que se avecina van a pasar cosas muy interesantes. En primer lugar, sale el procesador de AMD 3950x (demorado desde septiembre?). Luego vienen los Thread Ripper Zen 2 Series 3000. E Intel lo único que ha hecho es básicamente hacer un refresh de SkyLake-X para estar competitivos con AMD.

Ah pero esas gráficas que aseguran que son mejores sobre CPUs de años anteriores son muy buenas:



¿Pero las de este año que? Noticias emocionantes eh...
7  Foros Generales / Sugerencias y dudas sobre el Foro / Cloudflare firewall muy estricto en: 22 Abril 2016, 19:29 pm
Estuve tratando de poner una lista de comandos con ps en un post y la mayoría me los bloqueaba, por ejemplo:

ps aux

Es fácil de saltarselo, pero no se puede poner entre etiquetas code (al menos no sin distorsionar el contenido).

Edit: (Me había olvidado del otro tema....)
8  Foros Generales / Foro Libre / ¿Espacios o Tabuladores? en: 4 Marzo 2016, 03:31 am
Este tema es bastante común, pero no encuentro ninguno así en este foro. Si ya existe, que mencionen las referencias (el buscador no me dio nada).

Es cuestión de preferencia, pero quería saber la opinión del foro en cuanto a como le dan formato a el texto que escriben, sea código fuente o no. ¿Alinean las cosas con tabuladores o espacios? Lo dejaremos como opinión (aunque hay gente que defiende fervientemente uno o lo otro). Porfavor dejen la razón por la cual eligen una o lo otro.

Yo uso tabuladores (muy rara vez espacios). Siempre para indentar casi, no exactamente para alinear las cosas. Simplemente es mucho más sencillo moverse una predetermina cantidad de espacios con el tabulador y si alguien quiere cambiar la cantidad de espacios que usa el tabulador lo pueden hacer por mediante el editor de texto preferido.
9  Foros Generales / Sugerencias y dudas sobre el Foro / Pemitir acceso a rango de IP en: 19 Diciembre 2015, 16:55 pm
Quisiera saber si es posible quitar de la lista negra a 104.48.0.0/12. Es algo molesto tener que estar usando proxies o vpns constantemente. Son IPs de EUA y no figuran en la lista de países en la pagina de cloudflare. Me parece que en el pasado, ha habido ataques provenientes de ips de EUA pero han sido escasos?
10  Foros Generales / Foro Libre / ¿Ya tienes tu boleto para.... en: 16 Julio 2015, 23:54 pm




Advertencia: Este tema tiene escenas muy emotivas y pueden o no arruinarte la trama de la película/serie

Empezemos por Dragon Ball:


Y ahora veamos el gigante de hierro (mega spoiler):


Este es un pequeño corto acerca de un astronauta y dos granos en la luna:


MUFASA:


Pokemon la primera pelicula:


Ahora, yo se que este es un foro en español, pero esta historia es muy conmovedora. Narra la historia de Kaim (un guerrero maldecido a ser inmortal) y la niña de una posada a la cual atendía despues de todas sus batallas (inmortal = no se muere). La niña tiene una enfermedad de nacimiento que eventualmente la mata:


Son todos por ahora:

Páginas: [1] 2 3 4 5 6 7
WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines