Artículo original: Gitting Things Done – A Visual and Practical Guide to Git [Full Book]

Introducción

Git es increíble.

La mayoría de los desarrolladores de software usan Git en el día a día. Pero, ¿cuántos realmente entienden Git? ¿Sientes que sabes lo que está pasando por detrás a medida que usas Git para ejecutar varias tareas?

Por ejemplo, ¿qué sucede cuando usas git commit? ¿Qué se almacena entre las confirmaciones? ¿Es sólo un diff entre la confirmación actual y el anterior? Si es así, ¿cómo se codifica el diff? O, ¿es una copia instantánea entero cada vez que el repositorio se almacena?

La mayoría de las personas que usan Git no saben las respuestas a estas preguntas de arriba. Pero, ¿esto importa realmente? ¿Realmente tienes que saber todas esas cosas?

Yo argumentaría que sí importa. Como profesionales, deberíamos esforzarnos en entender las herramientas que usamos, especialmente si los usamos todo el tiempo, como Git.

Aún más preciso. He encontrado que entender realmente cómo funciona Git es beneficioso en muchos escenarios – si estás resolviendo conflictos de fusión, viendo cómo conducir un rebase interesante, o inclusive cuando algo va mal ligeramente.

Muchísimas veces he recibido preguntas sobre Git de gente con experiencia, ingenieros de software altamente talentosos. He visto desarrolladores maravillosos reaccionar con miedo cuando algo sucede en su historial de confirmaciones, y simplemente no saben qué hacer. Esto no tiene que ser así.

Al leer este libro, obtendrás una nueva perspectiva de Git. Te sentirás con confianza cuando trabajes con Git, y entenderás los mecanismos subyacentes de Git, al menos aquellos que son beneficiosos de entender. Le vas a captar.

Tabla de Contenidos

¿Para quién es este Libro?

Para cualquier desarrollador de software que quiera profundizar su conocimiento sobre Git.

Si ya tienes experiencia con Git – Estoy seguro que serás capaz de profundizar tus conocimientos. Inclusive si eres nuevo en Git - empezaré con una vista general del mecanismo de Git, y los términos usados a lo largo de este libro.

Este libro es para ti. Lo escribí así para que puedas aprender más sobre Git, y también llegues a apreciar, o inclusive amar a Git.

También notarás que uso un estilo casual en todo el libro. Creo que aprender Git debe de ser intuitivo y divertido. Aprender cosas nuevas siempre es difícil, y pensé que escribir en un estilo menos casual realmente no haría un buen servicio. Y como ya mencioné - Este libro es para ti.

¿Quién soy yo?

Este libro es sobre ti, y tu jornada con Git. Pero me gustaría decirte un poco sobre por qué pienso que puedo contribuir a tu jornada.

Soy el CTO y uno de los co-fundadores de Swimm.io, una herramienta de gestión de conocimiento para código. Parte de lo que hacemos es enlazar partes del código en repositorios de Git a partes de la documentación, y luego rastrear cambios en el repositorio para actualizar la documentación si es necesario.

En Swimm, tengo que diseccionar partes de Git, entender sus mecanismos subyacentes y también ganar intuición sobre por qué se implementa Git de la manera que se hace.

Antes de fundar Swimm practiqué enseñando en muchos entornos diferentes - entre ellos, gestionar el cyber track del Desafío Tech de Israel, fundar la Academia de Seguridad de Punto de Verificación (en inglés Check Point Security Academy), y escribir un libro completo de texto.

Este libro es mi intento de aprovechar lo máximo de ambos mundos - mi experiencia de enseñanza así como mi experiencia práctica profunda con Git, y darte la mejor experiencia de aprendizaje que puedo.

El enfoque de este libro

Este no es definitivamente el primer libro sobre Git. Cuando me senté a escribirlo, tuve tres principios en mente.

  1. Práctico - en este libro, aprenderás cómo lograr las cosas en Git. Cómo introducir cambios, cómo deshacerlos, y cómo arreglar las cosas cuando salen mal. Entenderás cómo funciona Git no sólo por el hecho de entender, sino con una mentalidad práctica. A veces me refiero a esto como el "principio práctico" - el cual me guía en decidir si incluyo ciertos tópicos, y en qué medida.
  2. A profundidad - te sumergirás en la forma de operar de Git, entender sus mecanismos. Construirás tu entendimiento gradualmente, y siempre enlazarás tus conocimientos a escenarios reales que podrías encontrar en tu trabajo. Para alcanzar un entendimiento a profundidad, casi siempre prefiero la línea de comandos en vez de interfaces gráficas, así realmente puedes ver qué comandos estoy ejecutando.
  3. Visual - a medida que me esfuerzo en proveerte con intuición, los capítulos estarán acompañados de ayudas visuales.

¿Por qué este libro está disponible públicamente?

Pienso que todos deberían tener acceso a contenido de alta calidad sobre Git, y me gustaría que este libro llegue a tantas personas como sea posible.

Si te gustaría apoyar este libro, eres bienvenido en comprar la versión física, un libro electrónico, o comprarme un café. ¡Gracias!

Videos Acompañantes

He cubierto muchos tópicos de este libro en mi canal de Youtube - @BriefVid. Eres bienvenido en verificarlos también.

A trabajar

A lo largo de este libro, mayormente usaré el singular en segunda persona - y directamente te escribiré a ti. También te pediré que trabajes, que ejecutes los comandos tú mismo, así llegas a sentir lo que es usar las cosas con Git, no sólo leerlo.

Los sentimientos de Git

A lo largo de este libro, a veces me refiero a Git con palabras tales como "cree", "piensa", o "quiere". Como puedes argumentar, Git no es humano, y no tiene sentimientos o creencias. Bueno, eso es verdad, pero para que nosotros disfrutemos al jugar con Git, y ayudarte en que disfrutes leer (y yo escribiendo), siento que referirme a Git más que sólo código lo hace muchos más divertido.

Mi configuración

Incluiré capturas de pantalla. No hay necesidad de que configures para coincidir con lo mío, pero si sientes curiosidad sobre mi configuración, entonces:

Los comentarios son bienvenidos

Este libro ha sido creado para ayudarte y las personas como tú aprendan, entiendan Git, y apliquen ese conocimiento en la vida real.

Desde el principio, pedí comentarios y fui afortunado en recibirlo de grandes personas (ve reconocimientos) para asegurarte que el libro alcanza estos objetivos. Si te gustó algo sobre este libro, sentiste que algo le faltó, o que algo necesita mejorar - Me encantaría escucharlo de ti. Por favor encuéntrame en: gitting.things@gmail.com.

Nota

Este libro es provisto gratuitamente en freeCodeCamp como se describe arriba y de acuerdo a la licencia Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Si te gustaría apoyar este libro, eres bienvenido en comprar la versión física, una versión electrónica, o comprarme un café. ¡Gracias!

Parte 1 - Objetos Principales e Introduciendo cambios

Capítulo 1 - Objetos de Git

Es tiempo de empezar tu jornada en las profundidades de Git. En este capítulo - empezamos con lo básico - aprenderás sobre los objetos de Git más importantes, y adoptar una forma de pensar sobre Git. ¡Vamos a ello!

Git como un Sistema para mantener un Sistema de archivos

Siendo que hay diferentes formas de usar Git, adoptaré aquí una forma que he aprendido que es la más clara y útil: viendo a Git como un sistema que mantiene un sistema de archivos, y específicamente - copias instantáneas de ese sistema de archivos con el tiempo.

Un sistema de archivos con un directorio raíz (en sistemas basados en UNIX, /), el cual usualmente contiene otros directorios (por ejemplo, /usr o /bin). Estos directorios contienen otros directorios, y/o archivos (por ejemplo, /usr/1.txt). En una máquina Windows, un directorio raíz de un disco sería C:\, y un sub-directorio podría ser C:\users. Adoptaré la convención de los sistemas basados en UNIX a lo largo de este libro.

Blobs

En Git, los contenidos de los archivos son almacenados en objetos llamados blobs, abreviado para binary large objects (objetos grandes de binario).

La diferencia entre blobs y archivos es que los archivos también contienen meta-datos. Por ejemplo, un archivo "recuerda" cuando fue creado, de esa forma si tu mueves ese archivo de un directorio a otro, su tiempo de creación permanece el mismo.

Los blobs, en contraste, son sólo corrientes de binario de datos, como el contenido de un archivo. Un blob no registra su fecha de creación, su nombre, o cualquier otra cosa más que su contenido.

Cada blob en Git es identificado por su hash SHA-1. Los hashes SHA-1 consisten de 20 bytes, usualmente representados por 40 caracteres en forma hexadecimal. A lo largo de este libro a veces mostraré sólo los primeros caracteres de ese hash. Como los hashes, y específicamente los hashes SHA-1 son ubicuos en Git, es importante que entiendas las características básicas de los hashes.

Hashes

Un hash es una función determinista y matemática unidireccional.

Determinista significa que la misma entrada proveerá la misma salida. Eso es - tomas una corriente de datos, ejecutas una función hash en esa corriente, y obtienes un resultado.

Por ejemplo, si provees la función hash SHA-1 con la corriente hello, obtendrás 0xaaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d. Si ejecutas la función hash SHA-1 nuevamente, desde una máquina diferente, y le provees los mismos datos (hello), obtendrás el mismo valor.

Git usa SHA-1 como su función hash para identificar objetos. Se basa en ella siendo determinista, y de esa forma un objeto siempre tendrá el mismo identificador.

Una función unidireccional es una función que es difícil de invertir dado una entrada. Eso es, es imposible (o al menos, muy difícil) de determinar, dado el resultado de la función hash (por ejemplo 0xaaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d), qué entrada produce ese resultado (en este ejemplo hello).

De vuelta a Git

De vuelta a Git - Blobs, como cualquier otro objeto de Git, tienen hashes SHA-1 asociados a ellos.

Blobs have corresponding SHA-1 values
Los Blobs tienen valores SHA-1 correspondientes

Como dije al principio, Git puede ser visto como un sistema para mantener un sistema de archivos. Sistemas de archivos consisten en archivos y directorios. Un blob es el objeto de Git representando los contenidos de un archivo.

Árboles

En Git, el equivalente de un directorio es un árbol. Un árbol es básicamente un listado de directorios, refiriéndose a blobs, así también como otros árboles.

Los árboles son identificados por sus hashes SHA-1 también. Referirse a estos objetos, sean blobs u otros árboles, sucede por medio del hash SHA-1 de los objetos.

A tree is a directory listing
Un árbol es un listado de directorios

Considera el dibujo de arriba. Fíjate que el árbol CAFE7 se refiere al blob F92A0 como el archivo pic.png. En otro árbol, el mismo blob podría tener otro nombre - pero siempre y cuando los contenidos sean lo mismo, será el mismo objeto blob, y todavía tendrá el mismo valor SHA-1.

A tree may contain sub-trees, as well as blobs
Un árbol podría contener sub-árboles, así también como blobs

El diagrama de arriba es equivalente a un sistema de archivos con un directorio raíz que tiene un archivo en /test.js, y un directorio llamado /docs consistiendo de dos archivos: /docs/pic.png, y /docs/1.txt.

Confirmaciones

Ahora es tiempo de tomar una copia instantánea de ese sistema de archivos – y almacenar todos los archivos que existieron en ese tiempo, juntamente con sus contenidos.

En Git, una copia instantánea es una confirmación. Un objeto de confirmación incluye un puntero al árbol principal (el directorio raíz del sistema de archivos), así también como otros meta-datos tales como el confirmador (el usuario que autorizó la confirmación), un mensaje de la confirmación, y el tiempo de la confirmación.

En la mayoría de los casos, una confirmación también tiene uno o más confirmaciones padres – la copia instantánea anterior (o copias instantáneas). Por supuesto, los objetos de confirmación son también identificados por sus hashes SHA-1. Estos son los hashes que probablemente estás acostumbrado de ver cuando usas los comandos tales como git log.

A commit is a snapshot in time. It refers to the root tree. As this is the first commit, it has no parents
Una confirmación es una copia instantánea en el tiempo. Se refiere al árbol raíz. Ya que este es la primer confirmación, no tiene padres

Cada confirmación retiene la copia instantánea completa, no solo las diferencias entre sí mismo y su confirmación o confirmaciones padres.

¿Cómo puede funcionar eso? ¿No significa que Git tiene que almacenar un montón de datos para cada confirmación?

Examina lo que pasa si cambias los contenidos de un archivo. Digamos que editas el archivo 1.txt, y agregas un signo de exclamación – eso es, cambiaste el contenido de HELLO WORLD, a HELLO WORLD!.

Bueno, este cambio significa que Git crea un nuevo objeto blob, con un nuevo hash SHA-1. Esto tiene sentido, ya que sha1("HELLO WORLD") es diferente de sha1("HELLO WORLD!").

Changing the blob results in a new SHA-1
Cambiando los resultados blob en un nuevo SHA-1

Ya que tienes un nuevo hash, entonces el listado del árbol debería también cambiar. Después de todo, tu árbol ya no apunta al blob 73D8A, sino al blob 62E7A. Desde el momento que cambies los contenidos del árbol, también cambias su hash.

The tree that points to the changed blob needs to change as well
El árbol que apunta al blob cambiado necesita cambiar también

Y ahora, ya que el hash de ese árbol es diferente, también necesitas cambiar el árbol padre – ya que el último ya no apunta al árbol CAFE7, sino al árbol 24601. Consecuentemente, el árbol padre también tendrá un nuevo hash.

The root tree also changes, and so does its hash
El árbol raíz también cambia, y así lo hace su hash

Casi listo para crear un nuevo objeto de confirmación, y parece que vas a almacenar un montón de datos – el sistema de archivos entero, ¡una vez más! ¿Pero es eso realmente necesario?

En realidad, algunos objetos, específicamente los objetos blob, no han cambiado ya que la confirmación anterior – el blob F92A0 permaneció intacto, y así también el blob F00D1.

Así que este es el truco – siempre y cuando un objeto no cambie, Git no lo almacena nuevamente. En este caso, Git no necesita almacenar el blob F92A0 o el blob F00D1 una vez más. Git se puede referir a ellos usando solamente sus valores hash. Entonces puedes crear tu objeto de confirmación.

Blobs that remained intact are referenced by their hash values
Blobs que permanecieron intactos son referenciados por sus valores hash

Ya que esta confirmación no es la primer confirmación, también tiene una confirmación padre – la confirmación A1337.

Considerando Hashes

Después de introducir blobs, árboles, y confirmaciones - considera los hashes de estos objetos. Digamos que escribí la cadena Git is awesome!, y de ahí creé un objeto blob. Tú hiciste lo mismo en tu sistema. ¿Tendríamos el mismo hash?

La respuesta es – Sí. Ya que los blobs consisten de los mismos datos, tendrán los mismos valores SHA-1.

¿Qué tal si hiciera un árbol que referencia al blob de Git is awesome!, y le diera un nombre específico y metadatos, y tú hicieras exactamente lo mismo en tu sistema? ¿Tendríamos el mismo hash?

De nuevo, sí. Ya que los objetos árboles son los mismos, tendrían el mismo hash.

¿Qué tal si creara una confirmación apuntando a ese árbol con el mensaje de confirmación Hello, y tú también hicieras lo mismo en tu sistema? ¿Tendrían el mismo hash?

En este caso, la respuesta es – No. Aunque nuestros objetos de confirmación se refieren al mismo árbol, tienen diferentes detalles de confirmación – tiempo, confirmador, y así sucesivamente.

¿Cómo son almacenados los Objetos?

Ahora entiendes el propósito de los blobs, los árboles y las confirmaciones. En los próximos capítulos, también crearás estos objetos tú mismo. A pesar de ser interesante, entender en realidad cómo estos objetos son codificados y almacenados no es vital para tu entendimiento.

Pequeña recapitulación - Objetos de Git

Para recapitular, en esta sección introdujimos tres objetos de Git:

  • Blob – contenidos de un archivo.
  • Árbol – un listado de directorios (de blobs y árboles).
  • Confirmación – una copia instantánea del árbol de trabajo.

En el próximo capítulo, entenderemos sobre las ramas en Git.

Capítulo 2 - Las ramas en Git

En el capítulo anterior, sugerí que deberíamos ver a Git como un sistema para mantener un sistema de archivos.

Una de las maravillas de Git es que permite a múltiples personas trabajar en ese sistema de archivos, en paralelo, (mayormente) sin interferir en el trabajo de otros. La mayoría de las personas dirían que están "trabajando en la rama X." ¿Pero qué significa eso en realidad?

Una rama es sólo una referencia nombrada a una confirmación.

Siempre puedes referenciar a una confirmación por su hash SHA-1, pero los humanos usualmente prefieren otras formas de llamar a los objetos. Una rama es una forma de referenciar a una confirmación, pero realmente es eso nada más.

En la mayoría de los repositorios, la línea principal del desarrollo está hecho en una rama llamada main. Esto es sólo un nombre, y es creado cuando usas git init, haciéndolo que sea ampliamente usado. Sin embargo, podrías usar cualquier otro nombre que quisieres.

Típicamente, la rama apunta a la última confirmación en la línea de desarrollo en el que estás trabajando actualmente.

A branch is just a named reference to a commit
Una rama es solo una referencia nombrada a una confirmación

Para crear otra rama, puedes usar el comando git branch. Cuando haces eso, Git crea otro puntero. Si creaste una rama llamada test, usando git branch test, estarías creando otro puntero que apunta a la misma confirmación como la rama en la que se encuentra:

Usando  crea otro puntero
Using git branch creates another pointer

¿Cómo sabe Git la rama en que te encuentras actualmente? Mantiene otro puntero designado, llamado HEAD. Usualmente, HEAD apunta a una rama, que a su vez apunta a una confirmación. En el caso descrito, HEAD podría apuntar a main, que a su vez apunta a la confirmación B2424. En algunos casos, HEAD también puede apuntar a una confirmación directamente.

 points to the branch you are currently on
HEAD points to the branch you are currently on

Para cambiar la rama activa a ser test, puedes usar el comando git checkout test, o git switch test. Ahora ya puedes adivinar lo que hace este comando en realidad – solo cambia a HEAD a que apunte a test.

 changes where  points
git checkout test changes where HEAD points

También podrías usar git checkout -b test antes de crear la rama test, el cual es el equivalente de ejecutar git branch test para crear la rama, y luego git checkout test para mover el HEAD a que apunte a la nueva rama.

Al punto representado en el dibujo de arriba, ¿qué sucedería si hicieras algunos cambios y crearas una nueva confirmación usando git commit? ¿A qué rama será agregada la nueva confirmación?

La respuesta es la rama test, ya que éste es la rama activa (ya que HEAD apunta a éste). Después, el puntero test se moverá a la nueva confirmación agregada recientemente. Fíjate que HEAD todavía apunta a test.

Every time we use , the branch pointer moves to the newly created commit
Every time we use git commit, the branch pointer moves to the newly created commit

Si vuelves atrás a main usando git checkout main, Git moverá el HEAD a que apunta a main nuevamente.

The resulting state after using
The resulting state after using git checkout main

Ahora, si creas otra confirmación, ¿a qué rama será agregado?

Así es, será agregado a la rama main (y su padre sería la confirmación B2424).

The resulting state after creating another commit on the  branch
The resulting state after creating another commit on the main branch

Pequeña recapitulación - Las ramas

  • Una rama es una referencia nombrada a una confirmación.
  • Cuando uses git commit, Git crea un objeto de confirmación, y mueve la rama a que apunte a la confirmación creada recientemente.
  • HEAD es un puntero especial que le dice a Git qué rama es la rama activa (en casos excepcionales, puede apuntar directamente a una confirmación).

En los próximos capítulos, aprenderás cómo introducir cambios a Git. Crearás un repositorio desde cero – sin usar git init, git add, o git commit. Esto te permitirá profundizar tu entendimiento de lo que está pasando por debajo cuando trabajes con Git. También crearás nuevas ramas, cambiarás ramas, y crearás confirmaciones adicionales – todo sin usar git branch o git checkout. No lo sé tú, pero yo ¡ya estoy entusiasmado!

Capítulo 3 - Cómo registrar cambios en Git

Hasta ahora, hemos aprendido sobre cuatros entidades distintas en Git:

  1. Blob –  contenidos de un archivo.
  2. Árbol – un lista de directorios (de blobs y árboles).
  3. Confirmación – una copia instantánea del árbol de trabajo, con algunos metadatos tales como el tiempo o el mensaje de confirmación.
  4. Rama – una referencia nombrada a una confirmación.

Los primeros tres son objetos, donde el cuarto es una forma de referirse a objetos (específicamente, confirmaciones).

Ahora, es tiempo de entender cómo introducir cambios en Git.

Cuando trabajas en tu código fuente, trabajas desde un directorio de trabajo. Un dir(ectorio) de trabajo (también llamado "árbol de trabajo") es cualquier directorio en tu sistema de archivos el cual tiene un repositorio asociado. Contiene las carpetas y los archivos de tu proyecto, y también un directorio llamado .git del cual hablaremos más adelante. Recuerda que dijimos que Git es un sistema para mantener un sistema de archivos. El directorio de trabajo es la raíz del sistema de archivos para Git.

Después de que haces algunos cambios, podrías querer registrarlos en tu repositorio. Un repositorio (en corto: "repo") es una colección de confirmaciones, cada uno de los cuales es un archivo de lo que el árbol de trabajo del proyecto parecía en una fecha anterior, sea en tu máquina o la de alguien más. Eso es, como dije antes, una confirmación es una copia instantánea del árbol de trabajo.

Un repositorio también incluye otras cosas además de tus archivos de código, tales como HEAD y ramas.

A working dir alongside the repository
Un directorio de trabajo junto al repositorio

Fíjate las convenciones del dibujo que uso: incluyo .git dentro del directorio de trabajo, para recordarte que es una carpeta dentro de la carpeta del proyecto en el sistema de archivos. La carpeta .git en realidad contiene los objetos del repositorio, como veremos en el capítulo 4.

Hay otros sistemas de control de versión donde los cambios son confirmados directamente desde el directorio de trabajo al repositorio. En Git, este no es el caso. En sí, los cambios son primero registrados en algo llamado el índice, o el área de staging.

Ambos términos se refieren a la misma cosa, son usados frecuentemente en la documentación de Git. Usaré estos términos indistintamente a lo largo de este libro, ya que te sentirás mas cómodo con ambos.

Puedes imaginarte el agregar cambios al índice como una forma de "confirmando" tus cambios, uno por uno, antes de crear una confirmación (el cual registra todos los cambios aprobados de una sola vez).

Cuando haces checkout a una rama, Git popula el índice y el directorio de trabajo con los contenidos de los archivos ya que existen en la confirmación a la rama al cual está apuntando. Cuando usas git commit, Git crea un nuevo objeto de confirmación basado en el estado de ese índice.

The three "states" - working dir, index, and repository
Los tres "estados" - directorio de trabajo, índice, y repositorio

Usando el índice te permite preparar cuidadosamente cada confirmación. Por ejemplo, podrías tener dos archivos con cambios en tu directorio de trabajo:

Working dir includes two files with changes
Working dir includes two files with changes

Por ejemplo, digamos que estos dos archivos son 1.txt y 2.txt. Es posible agregar solamente uno de ellos (por ejemplo, 1.txt) al índice, usando git add 1.txt:

The state after staging
The state after staging 1.txt

Como resultado, el estado del índice coincide con el estado del HEAD (en este caso, "Commit 2"), con la excepción del archivo 1.txt, el cual coincide el estado de 1.txt en el directorio de trabajo. Ya que no pusiste en el área de preparación a 2.txt, el índice no incluye a la versión actualizada de 2.txt. Así que el estado de 2.txt en el índice coincide con el estado de 2.txt en el "Commit 2".

En detrás de escena - una vez que pones en el área de preparación una versión de un archivo, Git crea un objeto blob con los contenidos del archivo. Este objeto blob es agregado luego al índice. Siempre y cuando solamente modifiques el archivo en el directorio de trabajo, sin ponerlo en el área de preparación, los cambios que hagas no son registrados en los objetos blob.

Cuando consideramos la figura previa, fíjate que no dibujo la versión del área de preparación del archivo como parte del "repositorio", ya que esta representación, el "repositorio" se refiere a un árbol de confirmaciones y sus referencias, y este blob no ha sido parte de ninguna confirmación.

Ahora, puedes usar git commit para registrar el cambio a 1.txt solamente:

The state after using
The state after using git commit

Usando git commit ejecuta dos operaciones principales:

  1. Crea un nuevo objeto de confirmación. Este objeto de confirmación refleja el estado del índice cuando ejecutaste el comando git commit.
  2. Actualiza a la rama activa para que apunte a la confirmación creada recientemente. En este ejemplo, el main ahora apunta al "Commit 3", el nuevo objeto de confirmación.

Cómo crear un repo – La Forma Convencional

Vamos a asegurarnos que entiendas cómo los términos que hemos introducidos se relacionan al proceso de crear un nuevo repositorio. Este es un vistazo rápido de alto nivel, antes de sumergirnos más profundamente en este proceso.

Inicializa un nuevo repositorio usando git init my_repo, y luego cambia tu directorio al repositorio usando cd my_repo:

git_init
git init

Usando tree -f .git puedes ver que ejecutando git init my_repo resultó en unos pocos sub-directorios dentro de .git. (El argumento -f incluye archivos en la salida del árbol).

Nota: si estás usando Windows, ejecuta tree /f .git.

The output of  after using
The output of tree -f .git after using git init

Crea un archivo dentro del directorio my_repo:

Creating
Creating f.txt

Este archivo está dentro de tu directorio de trabajo. Si ejecutas git status, verás este archivo no está rastreado:

The result of
The result of git status

Los archivos en tu directorio de trabajo puede ser uno de los dos estados: rastreados o no rastreados.

Los archivos rastreados son archivos que Git "conoce". Ellos estuvieron en la última confirmación, o están en el área de preparación ahora (eso es, están en el área staging).

Los archivos no rastreados son todos los demás – cualquier archivo en tu directorio de trabajo que no estuvieron en tu última confirmación, y que no están en tu área de preparación.

El nuevo archivo (f.txt) no está rastreado actualmente, ya que no lo has agregado al área de preparación, y no ha sido incluido en la confirmación anterior.

 is in the working directory (and untracked)
f.txt is in the working directory (and untracked)

Ahora puedes agregar este archivo al área de preparación (también referido como poner en staging este archivo) usando git add f.txt. Puedes verificar que ha sido puesto en el área de preparación ejecutando git status:

Adding the new file to the staging area
Adding the new file to the staging area

Así que ahora el estado del índice coincide con el directorio de trabajo:

The state after adding the new file
The state after adding the new file

Ahora puedes crear una confirmación usando git commit:

Committing an initial commit
Committing an initial commit

Si ejecutas git status nuevamente, verás que el estado está limpio - eso es, el estado de HEAD (el cal apunta a tu confirmación inicial) equivale al estado del índice, y también el estado del directorio de trabajo. Usando git log verás en realidad que HEAD apunta a main el cual a su vez apunta a la nueva confirmación:

The output of  after introducing the first commit
The output of `git log` after introducing the first commit

¿Ha cambiado algo dentro del directorio .git? Ejecuta tree -f .git para verificar:

A lot of things have changed within
A lot of things have changed within .git

Aparentemente, un montón ha cambiado. Es tiempo de sumergirnos más profundo en la estructura de .git y entender qué está pasando por detrás cuando ejecutas git init, git add o git commit. Eso es exactamente lo que cubrirá el próximo capítulo.

Recapitulación - Cómo registrar cambios en Git

Aprendiste sobre los tres "estados" distintos del sistema de archivos que Git mantiene:

  • Dir(ectorio) de trabajo (también llamado "árbol de trabajo") - cualquier directorio en tu sistema de archivos el cual tiene un repositorio asociado.
  • Índice, o el Área de preparación - un espacio de interacción para la próxima confirmación.
  • Repositorio (en corto: "repo") - una colección de confirmaciones, el cual cada una es una copia instantánea del árbol de trabajo.

Cuando introduces cambios en Git, casi siempre sigue este orden:

  1. Cambias el directorio de trabajo primero
  2. Luego pones estos cambios en el área de preparación (o algunos de ellos) al índice
  3. Y finalmente, confirmas estos cambios - de esta forma actualiza el repositorio con una nueva confirmación. El estado de esta nueva confirmación coincide con el estado del índice

¿Listo para sumergirte más profundo?

Capítulo 4 - ¿Cómo crear un repo desde cero?

Hasta ahora hemos cubiertos algunos fundamentos de Git, y ahora deberías estar listo para realmente continuar con Git.

Para entender profundamente cómo funciona Git, crearás un repositorio, pero esta vez – lo construirás desde cero. Como en otros capítulos, te animo a intentar los comandos a lo largo de este capítulo.

Cómo configurar .git

Crea un nuevo directorio, y ejecuta git status dentro:

 in a new directory
git status in a new directory

Muy bien, parece que Git no está feliz ya que no tiene una carpeta .git todavía. Lo natural por hacer sería crear ese directorio e intentarlo otra vez:

 after creating
git status after creating .git

Aparentemente, crear un directorio .git no es suficiente. Necesitas agregar algún contenido a ese directorio.

Un repositorio de Git tiene dos conceptos principales:

  • Una colección de objetos – blobs, árboles, y confirmaciones.
  • Un sistema de nombramiento de esos objetos – llamados referencias.

Un repositorio también podría contener otras cosas, tales como hooks, pero a lo menos – debe incluir objetos y referencias.

Crear un repositorio para los objetos en .git/objets, y un directorio para las referencias (en forma corta: "refs") en .git/refs (en sistemas Windows – .git\objects y .git\refs, respectivamente).

Considering the directory tree
Considering the directory tree

Un tipo de referencia son las ramas. Internamente, Git llama a las ramas por el nombre de heads. Crea un directorio para las ramas – .git/refs/heads.

The directory tree
The directory tree

Esto todavía no cambia el resultado de git status:

 after creating
git status after creating .git/refs/heads

¿Cómo sabe Git dónde comenzar cuando busca una confirmación en el repositorio? Como expliqué antes, busca el HEAD, el cual apunta a la rama actual activa (o la confirmación, en algunos casos).

Así que, necesitas crear el HEAD, el cual es sólo un archivo que reside en .git/HEAD. Puedes aplicar lo siguiente:

En UNIX:

echo "ref: refs/heads/main" > .git/HEAD

En Windows:

echo ref: refs/heads/main > .git\HEAD

Así que ahora sabes cómo se implementa HEAD – es simplemente un archivo, y su contenido describe a qué apunta.

Siguiendo el comando de arriba, git status parece cambiar su pensamiento:

 is just a file
HEAD is just a file

Fíjate que Git "cree" que estás en la rama llamada main, aunque no has creado esta rama. main es sólo un nombre. También puedes hacer creer a Git que estás en una rama llamada banana si quieres:

Creating a branch named
Creating a branch named banana

Vuelve al main, ya que continuarás trabajando (mayormente) allí a lo largo de este capítulo, sólo para aderirnos a la convención regular:

echo "ref: refs/heads/main" > .git/HEAD

Ahora que tienes tu directorio .git listo, puedes trabajar a tu manera para hacer una confirmación (de nuevo, sin usar git add o git commit).

Comandos de plomería vs comandos de porcelana en Git

A este punto, debería ser bueno hacer una distinción entre dos tipos de comandos de Git: de plomería y de porcelana. La aplicación de los términos extrañamente viene de los baños, tradicionalmente los hechos de porcelana, y la infraestructura de la plomería (las cañerías y los drenajes).

La capa de porcelana provee una interfaz amigable para el usuario en la plomería. La mayoría de la gente solamente manejan la porcelana. Aún así, cuando las cosas van (terriblemente) mal, y alguien quiere entender el por qué, tendrían que remangarse y tratar con la plomería.

Git usa esta terminología como una analogía para separar los comandos de bajo nivel que los usuarios usualmente no necesitan usar directamente (comandos "de plomería") de los comandos de alto nivel (comandos "de porcelana").

Hasta ahora, has lidiado con comandos de porcelana – git init, git add o git commit. Es tiempo de ir más profundo, y que te familiarices con algunos comandos de plomería.

Cómo crear objetos en Git

Empieza por crear un objeto y escríbelo dentro de la base de datos de los objetos de Git, que reside dentro de .git/objects. Para saber el valor del hash SHA-1 de un blob, puedes usar git hash-object. (Sí, un comando de plomería), de la siguiente forma:

En UNIX:

echo "Git is awesome" | git hash-object --stdin

En Windows:

> echo Git is awesome | git hash-object --stdin

Usando --stdin estás instruyendo a git hash-object que tome su entrada de la entrada estándar. Esto te proveerá con el valor del hash relevante:

Getting a blob's SHA-1
Getting a blob's SHA-1

En realidad para escribir ese blob dentro de la base de datos de objetos de Git, puedes agregar el conmutador -w por git hash-object. Después, verifica el contenido de la carpeta .git, y mira si han cambiado:

Writing a blob to the objects' database
Writing a blob to the objects' database

Puedes ver que el hash de tu blob es 7a9bd34a0244eaf2e0dda907a521f43d417d94f6. También puedes ver que un directorio ha sido creado como .git/objects, un directorio llamado 7a, y dentro, un archivo con el nombre de 7a9bd34a0244eaf2e0dda907a521f43d417d94f6.

Lo que Git hizo aquí es tomar los primeros dos caracteres del hash SHA-1, y usarlos como el nombre de un directorio. Los caracteres sobrantes son usados como el nombre de archivo para el archivo que en realidad contiene el blob.

¿Por qué eso es así? Considera un repositorio considerablemente grande, uno que tiene 400,000 objetos (blobs, árboles, y confirmaciones) en su base de datos. Buscar un hash dentro de esa lista de 400,000 hashes podría tomar un buen rato. Así, Git simplemente divide ese problema por 256.

Para buscar el hash de arriba, Git primero miraría el directorio llamado 7a dentro del directorio .git/objects, el cual tendría hasta 256 directorios (00 hasta FF). Luego, buscaría dentro de ese directorio, acortando la búsqueda a medida que continúa.

De vuelta al proceso de generar una confirmación. Has creado un objeto solamente. ¿Cuál es el tipo de ese objeto? Puedes usar otro comando de plomería, git cat-file -t (-t significa "type" - "tipo"), para verificar eso:

Using  reveals the type of the Git object
Using git cat-file -t <object_sha> reveals the type of the Git object

No sorprendentemente, este objeto es un blob. También puedes usar git cat-file -p (-p significa "pretty-print" - "imprimir-bonito") para ver su contenido:

cat_file_p_blob
git cat-file -p

Este proceso de crear un objeto blob como .git/objects usualmente sucede cuando agregas algo al área de preparación – eso es, cuando usas git add. Así que los blobs no son creados cada vez que guardes un archivo al sistema de archivos (el directorio de trabajo), sino solamente cuando lo pongas en el área de preparación.

Recuerda que Git crea un blob del archivo entero que se pone en el área de preparación. Inclusive si un solo caracter es modificado o agregado, el archivo tiene un nuevo blob con un nuevo hash (como en el ejemplo del capítulo 1 donde agregaste ! al final de la línea).

¿Habrá algún cambio a git status?

 after creating a blob object
git status after creating a blob object

Aparentemente, no. Agregar un objeto blob a la base de datos interno de Git no cambia el estado, ya que Git no sabe de ningún archivo con seguimiento (o sin seguimiento) en esta área de preparación.

Necesitas rastrear este archivo – agregarlo al área de preparación. Para hacer eso, puedes usar otro comando de plomería, git update-index, así:

git update-index --add --cacheinfo 100644 <blob-hash> <filename>

Nota: El cacheinfo es un modo de archivo de 16-bits almacenado por Git, siguiendo el diseño de los tipos y modos de POSIX. Esto no está dentro del enfoque de este libro, ya que no es realmente importante para que hagas las cosas.

Ejecutar el comando de arriba resultará en un cambio al contenido de .git:

The state of  after updating the index
The state of .git after updating the index

¿Puedes encontrar el cambio? Un nuevo archivo con el nombre de index ha sido creado. Eso es todo – el famoso índice (o área de preparación), es básicamente un archivo que reside dentro de .git/index.

Así que ahora que tu blob ha sido agregado al índice, ¿esperas que git status luzca diferente?

 after using
`git status` after using git update-index

¡Eso es interesante! Dos cosas pasaron aquí.

Primero, puedes ver que awesome.txt aparece en verde, en el área "Changes to be committed" ("Cambios a ser confirmados"). Eso es así porque el índice ahora incluye a awesome.txt, esperando a ser confirmado.

Segundo, podemos ver que awesome.txt aparece en rojo – porque Git cree que el archivo awesome.txt ha sido eliminado, y el hecho que el archivo ha sido eliminado sino que no está en el área de preparación.

(Nota: te habrás dado cuenta que a veces me refiero a Git con palabras tales como "cree", "piensa", o "quiere". Como expliqué en la introducción de este libro - para que disfrutemos jugando con Git, y leer (y escribir) este libro, siento que referirse a Git más que sólo código lo hace mucho más divertido.)

Esto sucede ya que agregaste el blob con el contenido Git is awesome a la base de datos de los objetos, y actualizaste el índice que el archivo awesome.txt retiene el contenido de ese blob, pero en realidad nunca creaste ese archivo en el disco.

Fácilmente puedes resolver esto tomando el contenido de ese blob y escribirlo a nuestro sistema de archivos, a un archivo llamado awesome.txt:

echo "Git is awesome" > awesome.txt

Como resultado, ya no aparecerá en rojo por git status:

 after creating  on disk
git status after creating awesome.txt on disk

Así que ahora es tiempo de crear un objeto de confirmación desde tu área de preparación. Como expliqué en el capítulo 1, un objeto de confirmación tiene una referencia a un árbol, así que necesitas crear un árbol.

Puedes lograr esto usando el comando git write-tree, el cual registra los contenidos del índice en un objeto árbol. Por supuesto, puedes usar git cat-file -t para ver que en sí es un árbol:

Creating a tree object with the contents of the index
Creating a tree object with the contents of the index

Y puedes usar git cat-file -p para ver su contenido:

 to see the tree's contents
git cat-file -p to see the tree's contents

Genial, así que creaste un árbol, y ahora necesitas crear un objeto de confirmación que referencia a este árbol. Para hacer eso, puedes usar el comando:

git commit-tree <tree-hash> -m <commit message>
Committing using the tree object
Committing using the tree object

Ahora deberías sentirte cómodo con los comandos usados para verificar el tipo de objeto creado, e imprimir su contenido:

Creating a commit object
Creating a commit object

Fíjate que este objeto de confirmación no tiene un padre, porque no es la primer confirmación. Cuando agregas otra confirmación probablemente querrás declarar su padre – no te preocupes, lo harás así más tarde.

El último hash que tuvimos – b6d05ee40344ef5d53502539772086da14ad2b07 – es un hash de la confirmación. En realidad deberías estar acostumbrado en usar estos hashes – probablemente los ves todo el tiempo (cuando usas git log, por ejemplo). Fíjate que el objeto de confirmación apunta a un objeto de árbol, con su propio hash, el cual raramente especificas explícitamente.

¿Cambiará algo en git status?

 after creating a commit object
git status after creating a commit object

No, nada ha cambiado. ¿Por qué es eso?

Bueno, para saber que tu archivo ha sido confirmado, Git necesita saber sobre la última confirmación. ¿Cómo sabe Git eso? Va al HEAD:

Looking at the contents of
Looking at the contents of HEAD

HEAD apunta a main, pero, ¿qué es main? Aún no lo has creado.

Como explicamos en el capítulo 2, una rama es simplemente una referencia nombrada a una confirmación. Y en este caso, nos gustaría que main se refiriera al objeto de confirmación con el hash b6d05ee40344ef5d53502539772086da14ad2b07.

Puedes lograr esto creando un archivo en .git/refs/heads/main, con el contenido de este hash, así:

Creating
Creating main

En resumen, una rama es sólo un archivo dentro de .git/refs/heads, que contiene un hash de la confirmación a la que se refiere.

Ahora, finalmente, git status y git log parecen apreciar nuestros esfuerzos:

git_status_commit_1
git status
git_log_commit_1
git log

¡Has creado con éxito una confirmación sin usar comandos de porcelana! ¿No es genial eso?

Recapitulación - Cómo crear un repo desde cero

En este capítulo, sin miedo te sumergiste en profundidad en Git. Paraste de usar comandos de porcelana y te cambiaste a los comandos de plomería.

Al usar echo y comandos de nivel bajo tales como git hash-object, fuiste capaz de crear un blob, agregarlo al índice, crear un árbol del índice, y crear un objeto de confirmación apuntando a ese árbol.

También aprendiste que HEAD es un archivo, localizado en .git/HEAD. Las ramas son archivos también, localizados en .git/refs/heads. Cuando entiendes cómo opera Git, esas nociones abstractas de HEAD o "ramas" se vuelven muy tangibles.

En el próximo capítulo profundizarás tu entendimiento de cómo trabajan las ramas por detrás.

Capítulo 5 - Cómo trabajar con las ramas en Git – Por debajo

En el capítulo anterior creaste un repositorio y una confirmación sin usar git init, git add o git commit. En este capítulo, crearemos y cambiaremos entre ramas sin usar comandos de porcelana (git branch, git switch, o git checkout).

Es perfectamente entendible si estás emocionado, ¡yo también lo estoy!

Continuando con lo del capítulo anterior - solamente tienes una rama, llamada main. Para crear otra con el nombre de test (como el equivalente de git branch test), necesitarías crear un archivo llamado test dentro de .git/refs/heads, y el contenido de ese archivo sería el mismo hash de la confirmación al que apunta la rama main.

Creating  branch
Creating test branch

Si usas git log, puedes ver que esto en sí es el caso – ambos main y test apuntan a esta confirmación:

 after creating  branch
git log after creating test branch

(Nota: si ejecutas este comando y no ves una salida válida, podrías haber escrito algo más que el hash de la confirmación en .git/refs/heads/test.)

Siguiente, cambiar a nuestra rama creada recientemente (el equivalente de git checkout test). ¿Cómo harías eso? Intenta responderte a ti mismo antes de que nos movamos al siguiente párrafo.

Para cambiar la rama activa, debería cambiar el HEAD a que apunte a tu nueva rama:

Switching to branch  by changing
Switching to branch test by changing HEAD

Como puedes ver, git status confirma que HEAD ahora apunta a test, el cual es, por lo tanto, la rama activa.

Ahora puedes usar los comandos que ya has usado en el capítulo anterior para crear otro archivo y agregarlo al índice:

Writing and staging another file
Writing and staging another file

Siguiendo los comandos de arriba, tu:

  • Creas un blob con el contenido de Another file (usando git hash-object).
  • Agregas al índice con el nombre de another_file.txt (usando git update-index).
  • Creas un archivo correspondiente en el disco con el contenido del blob (usando git cat-file -p).
  • Creas un objeto árbol representando al índice (usando git write-tree).

Ahora es tiempo de crear una confirmación que referencie a este árbol. Esta vez, también deberías especificar el padre de esta confirmación – el cual debería ser la confirmación anterior. Especificas el padre usando el argumento -p de git commit-tree:

Creating another commit object
Creating another commit object

Hemos creado una confirmación, con un árbol así también como un padre, como puedes ver:

Observing the new commit object
Observing the new commit object

¿git log nos mostrará la nueva confirmación?

 after creating "Commit 2"
git log after creating "Commit 2"

Como puedes ver, git log no nos muestra nada nuevo. ¿Por qué es eso?

Recuerda que git log rastrea las ramas para encontrar confirmaciones relevantes para mostrar. Nos muestra ahora test y la confirmación a la que apunta, y también nos muestra a main el cual apunta a la misma confirmación.

Así es – necesitas hacer que test apunte al nuevo objeto de confirmación. Puedes hacer eso cambiando el contenido de .git/refs/heads/test:

echo 22267a945af8fde78b62ee7f705bbecfdd276b3d > .git/refs/heads/test

Y ahora si ejecutas git log:

 after updating  branch
git log after updating test branch

¡Funcionó!

git log va al HEAD, el cual le dice a Git que vaya a la rama test, el cual apunta a la confirmación 222..3d, el cual se enlaza a su confirmación padre b6d..07.

Admira la belleza de Git 😊

Al inspeccionar la carpeta de tu repositorio, puedes ver que tienes seis objetos distintos en la carpeta .git/objects - estos son los dos blobs que creaste (uno para awesome.txt y uno para file.txt), dos objetos de confirmación ("Commit 1" y "Commit 2"), y los tres objetos - cada uno apuntado por uno de los objetos de confirmación.

The tree listing after creating "Commit 2"
The tree listing after creating "Commit 2"

También tienes a .git/HEAD que apunta a la rama o confirmación activa, y dos ramas - dentro de .git/refs/heads.

Recapitulación - Cómo trabajar con las ramas en Git – Por debajo

En este capítulo entendiste cómo trabajan realmente las ramas en Git.

Las cosas principales que cubrimos:

  • Una rama es un archivo en la carpeta .git/refs/heads, donde el contenido del archivo es una valor SHA-1 de una confirmación.
  • Para crear una nueva rama, Git simplemente crea un nuevo archivo en la carpeta .git/refs/heads con el nombre de la rama - por ejemplo, .git/refs/heads/my_branch para la rama my_branch.
  • Para cambiar la rama activa, Git modifica el contenido de .git/HEAD para referirse a la nueva rama activa. .git/HEAD también podría apunta a un objeto de confirmación directamente.
  • Cuando se confirma usando git commit, Git crea un objeto de confirmación, y también mueve la rama actual (eso es, el contenido del archivo en .git/refs/heads) a que apunte al objeto de confirmación recientemente creado.

Parte 1 - Resumen

Esta parte te introdujo lo interno de Git. Empezamos cubriendo los objetos básicos – blobs, árboles, y confirmaciones.

Aprendiste que un blob contiene el contenido de un archivo. Un árbol es un listado de directorios, conteniendo blobs y/o sub árboles. Una confirmación es una copia instantánea de nuestro directorio de trabajo, con algunos meta datos tales como el tiempo o el mensaje de confirmación.

Aprendiste sobre las ramas, viendo que no son más que una referencia nombrada a una confirmación.

Aprendiste el proceso de registrar cambios en Git, y que involucra el directorio de trabajo, un directorio que tiene un repositorio al que se asocia, el área de preparación (índice) el cual contiene el árbol para la próxima confirmación, y el repositorio, el cual es una colección de confirmaciones y referencias.

Clarificamos cómo estos términos se relacionan a los comandos de Git que conocemos al crear un nuevo repositorio y confirmando un archivo usando los comandos bien conocidos git init, git add y git commit.

Luego creaste un nuevo repositorio desde cero, usando echo y comandos de bajo nivel tales como git hash-object. Creaste un blob, le agregaste al índice, creaste un objeto de árbol representando al índice, e inclusive creaste un objeto de confirmación apuntando a ese árbol.

También fuiste capaz de crear y cambiar entre ramas modificando los archivos directamente. ¡Felicitaciones para aquellos que lo intentaron por sí mismos!

Todo junto ya, después de seguirme en esta parte, deberías sentir que has profundizado tu entendimiento de lo que está pasando por debajo cuando trabajas con Git.

La próxima parte explorarás diferentes estrategias para integrar cambios cuando se trabaja en diferentes ramas en Git - específicamente, merge y rebase.

Parte 2 - Ramificando e Integrando Cambios

Capítulo 6 - Diffs y Parches

En la parte 1 aprendiste cómo funciona Git por detrás, los diferentes objetos de Git, y cómo crear un repo desde cero.

Cuando los equipos trabajan con Git, introducen secuencias de cambios, usualmente en ramas, y luego necesitan combinar diferente historiales de cambios juntos. Para realmente entender cómo se logra esto, deberías aprender cómo trata Git a los diffs y a los parches. Luego aplicarás tus conocimientos para entender el proceso de merge y rebase.

Muchos de los procesos interesantes de Git como fusión, rebasing, o inclusive confirmar están basados en los diffs y parches. Los desarrolladores trabajan con diffs todo el tiempo, estés usando Git directamente o basándote en la vista diff del IDE. En este capítulo, aprenderás cómo son los diffs y parches, su esctructura, y cómo aplicar parches.

Como recordatorio del capítulo de Objetos de Git, una confirmación es una copia instantánea del árbol de trabajo en un cierto punto en el tiempo, además de algunos metadatos.

Aún así, es realmente difícil entender las confirmaciones individuales al mirar el árbol entero de trabajo. En sí, es más útil mirar cuán diferente es una confirmación de su confirmación padre, eso es, la diff entre estas confirmaciones.

Así que, ¿a qué me refiero cuando digo "diff"? Empecemos con algo de historia.

La historia de diff de Git

El diff de Git está basado en la utilidad diff de sistemas UNIX. diff fue desarrollado a principios de los '70 en el sistema operativo Unix. La primer versión lanzada se envió con la Quinta Edición de Unix en 1974.

git diff es un comando que toma dos entradas, y calcula la diferencia entre ellos. Las entradas pueden ser confirmaciones, pero también archivos, e inclusive archivos que nunca han sido introducidos al repositorio.

Git diff takes two inputs, which can be commits or files
Git diff takes two inputs, which can be commits or files

Esto es importante - git diff calcula la diferencia entre dos cadenas, el cual la mayor de las veces llega a consistir en código, pero no necesariamente.

Tiempo de manos a la obra

Como siempre, se te anima a que ejecutes los comandos tu mismo al leer este capítulo. A menos que se diga otra cosa, usaré el siguiente repositorio:

https://github.com/Omerr/gitting_things_repo.git

Lo puedes clonar localmente y tener el mismo punto de comienzo que estoy usando para este capítulo.

Considera este archivo de texto corto en mi máquina, llamado file.txt, el cual consiste de 6 líneas:

 consists of six lines
file.txt consists of six lines

Ahora, modifica este archivo un poco. Quita la segunda línea, e inserta una nueva línea como la línea cuatro. Agrega un signo de admiración (!) al final de la última línea, así obtienes este resultado:

After modifying , we get different six lines
After modifying file.txt, we get different six lines

Guarda este archivo con un nuevo nombre, new_line.txt.

Ahora puedes ejecutar git diff para calcular la diferencia entre los archivos así:

git diff --no-index file.txt new_file.txt

(Explicaré el argumento --no-index de este comando más tarde. Por ahora es suficiente saberlo para que nos permita comparar dos archivos que no son partes de un repositorio de Git.)

The output of
The output of git diff --no-index file.txt new_file.txt

La salida de git diff muestra bastante cosas.

Enfócate en la parte empezando con This is a file. Puedes ver que la línea agregada (// new test) es precedida por un signo +. La línea eliminada es precedida por un símbolo -.

Curiosamente, fíjate que Git ve a la línea modificada como una secuencia de dos cambios - borrar una línea y agregar una nueva. Así que el parche incluye eliminar la última línea, y agregar una nueva línea que es igual a esa línea, con la suma de un !.

Addition lines are preceded by , deletion lines by , and modification lines are sequences of deletions and additions
Addition lines are preceded by +, deletion lines by -, and modification lines are sequences of deletions and additions

Ahora sería bueno discutir los términos "patch" y "diff". Estos dos son usados con frecuencia indistintamente, aunque hay una distinción, al menos históricamente.

Un diff muestra las diferencias entre dos archivos, o copias instantáneas, y puede ser bastante mínimo en hacerlo así. Un patch (parche) es una extensión de un diff, aumentado con más información tales como líneas de contexto y nombre de archivos, el cual le permite ser aplicado más ampliamente. Es un documento de texto que describe cómo alterar un archivo existente o base de código.

En estos días, el programa diff de Unix, y git diff, pueden producir parches de varios tipos.

Un parche es una representación compacta de las diferencias entre dos archivos. Describe cómo convertir un archivo en otro.

En otras palabras, si aplicas las "instrucciones" producidas por git diff en file.txt - eso es, quitar la segunda línea, insertar // new test como la cuarta línea, quitar la última línea, y agregar una línea con el mismo contenido y ! - tendrás el contenido de new_file.txt.

Otra cosa importante a notar es que un parche es asimétrico: el parche de file.txt a new_file.txt no es el mismo que el parche para la otra dirección. Generar un parche entre new_file.txt y file.txt, en este orden, significaría exactamente las instrucciones opuestas que antes - agregar la segunda línea en vez de quitarla, y así sucesivamente.

A patch consists of asymmetric instructions to get from one file to another
A patch consists of asymmetric instructions to get from one file to another

Inténtalo:

git diff --no-index new_file.txt file.txt
Running git diff in the reverse direction yields the reverse instructions - add a line instead of removing it, and so on
Running git diff in the reverse direction yields the reverse instructions - add a line instead of removing it, and so on

El formato del parche usa contexto, así también como números de líneas, para localizar diferentes regiones de archivos. Esto permite a un parche ser aplicado a alguna versión más antigua o más reciente de la primera línea a aquel del que se derivó, siempre y cuando el programa que se aplica aún pueda localizar el contexto del cambio. Veremos cómo son usados exactamente.

La Estructura de un Diff

Es tiempo de indagar más profundamente.

Genera un diff de file.txt a new_file.txt nuevamente, y considera la salida más cuidadosamente:

git diff --no-index file.txt new_file.txt
The output of
The output of git diff --no-index file.txt new_file.txt

La primer línea introduce los archivos comparados. Git siempre le da a un archivo el nombre de a, y al otro el nombre de b. Así que en este caso file.txt se llama a, donde new_file.txt se llama b.

The first line in 's output introduces the files being compared
The first line in diff's output introduces the files being compared

Luego la segunda línea, comenzando con index, incluye los SHAs del blob de estos archivos. Así que aunque en nuestro caso inclusive no se guardan dentro de un repo de Git, Git muestra sus valores SHA-1 correspondientes.

El tercer valor en esta línea, 100644, es el "modo bits", indicando que esto es un archivo "regular": no ejecutable y no es un enlace simbólico.

El uso de dos puntos (..) aquí entre los SHAs del blob es sólo como un separador (a diferencia de otros casos donde se usa dentro de Git).

The second line in 's output includes the blob SHAs of the compared files, as well as the mode bits
The second line in diff's output includes the blob SHAs of the compared files, as well as the mode bits

Otras líneas de encabezado pueden indicar el modo bits antiguo y nuevo si han cambiado, nombres de archivos antiguos y nuevos si los archivos fueron renombrados, y así sucesivamente.

Los SHAs del blob (también llamados "blob IDs") son útiles si este parche luego se aplica por Git al mismo proyecto y hay conflictos al aplicarse. Entenderás mejor qué significa esto cuando aprendas sobre las fusiones en el próximo capítulo.

Luego de los IDs del blob, tenemos dos líneas: una comenzando con signos -, y los otros comenzando con signos +. Este es el encabezado "diff unificado" tradicional, de nuevo mostrando los archivos siendo comparados y la dirección de los cambios: signos - muestra líneas en la versión A que faltan de la versión B, y signos - muestra líneas que faltan en la versión A pero que están presentes en el B.

Si el parche de este archivo estuviera siendo agregado o eliminado en su totalidad, entonces uno de estos serían /dev/null para señalar eso.

 signs show lines in the A version but missing from the B version; and  signs, lines missing in A version but present in B
- signs show lines in the A version but missing from the B version, and + signs, lines missing in A version but present in B

Considera el caso donde eliminas un archivo:

rm awesome.txt

Y luego usa git diff:

's output for a deleted file
git diff's output for a deleted file

La versión A, representando el estado del índice, es actualmente awesome.txt, comparado al directorio de trabajo donde este archivo no existe, así que es /dev/null. Todas las líneas son precedidas por signos - ya que existen solamente en la versión A.

Por ahora, deshace la eliminación (más sobre deshaciendo cambios en la Parte 3):

git restore awesome.txt

Volviendo al diff con el que comenzamos:

The output of
The output of git diff --no-index file.txt new_file.txt

Después de este encabezado diff unificado, llegamos a la parte principal del diff, consistiendo de "secciones de diferencia", también llamados "hunks" o "chunks" en Git. Fíjate que estos términos son usados indistintamente, y podrías encontrarte con uno de ellos en la documentación y tutoriales de Git, así también como código fuente de Git.

Cada hunk comienza con una sola línea, comenzando con dos signos @. Estos signos son seguidos en la mayoría de veces por cuatro números, y luego un encabezado para el chunk - el cual es una suposición educada por Git. Usualmente, incluirá el comienzo de una función o una clase, cuando fuese posible.

En este ejemplo no incluye nada ya que este es un archivo de texto, así que considera otro ejemplo por un momento:

git diff --no-index example.py example_changed.py
When possible, Git includes a header for each hunk, for example a function or class definition
When possible, Git includes a header for each hunk, for example a function or class definition

En la imagen de arriba, el encabezado del hunk incluye el comienzo de la función que incluye las líneas cambiados - def example_function(x).

De vuelta a nuestro ejemplo previo:

Back to the previous diff
Back to the previous diff

Después de dos signos @, encontrarás cuatro números:

Los primeros números son precedidos por un signo - ya que se refieren a file A. El primer número representa el número de línea correspondiente a la primer línea en file A a la que este hunk se refiere. En el ejemplo de arriba, es 1, lo que significa que la línea This is a file corresponde al número de línea 1 en la versión de file A.

Este número es seguido por una coma (,), y luego el número de líneas del que este chunk consiste en el file A. Este número incluye todas las líneas de contexto (las líneas precedidas con un espacio en el diff), o líneas marcadas con un signo -, ya que son parte de file A, pero no las líneas marcadas con un signo +, ya que no existen en file A.

En nuestro ejemplo, este número es 6, contando la línea de contexto This is a file, la línea - es It has a nice poem:, luego las tres líneas de contexto, y por último Are belong to you.

Como puedes ver, las líneas que comienzan con un caracter de espacio son líneas de contexto, lo que significa que aparecen como se muestran en ambos file A y file B.

Luego, tenemos un signo + para marcar a los dos números que se refieren a file B. Primero, está el número de línea correspondiente a la primer línea en file B, seguido del número de líneas del que este chunk consiste en file B.

Este número incluye todas las líneas de contexto, así también como las líneas marcadas con el signo +, ya que son parte de file B, pero no las líneas marcadas con un signo -.

Estos cuatros números son seguidos por dos signos @ adicionales.

Después del encabezado del chunk, obtenemos las líneas actuales - sean las líneas de contexto, - o +.

Típicamente y por defecto, un hunk comienza y termina con tres líneas de contexto. Por ejemplo, si modificas las líneas 4-5 en un archivo con diez líneas:

  • Línea 1 - línea de contexto (antes de las líneas cambiadas)
  • Línea 2 - línea de contexto (antes de las líneas cambiadas)
  • Línea 3 - línea de contexto (antes de las líneas cambiadas)
  • Línea 4 - línea cambiada (antes de las líneas cambiadas)
  • Línea 5 - otra línea cambiada
  • Línea 6 - línea de contexto (después de las líneas cambiadas)
  • Línea 7 - línea de contexto (después de las líneas cambiadas)
  • Línea 8 - línea de contexto (después de las líneas cambiadas)
  • Línea 9 - esta línea no será parte del hunk

Así que por defecto, cambiar las líneas 4-5 resulta en un hunk consistiendo de las líneas 1-8, eso es, tres líneas antes y tres líneas después de las líneas modificadas.

Si ese archivo no tiene nueve líneas, sino seis líneas - entonces el hunk contendrá solamente una línea de contexto después de las líneas cambiadas, y no tres. De forma similar, si cambias la segunda línea de un archivo, entonces habría solamente una línea de contexto antes de las líneas cambiadas.

The patch format by
The patch format by git diff

Cómo producir diffs

El último ejemplo que consideramos muestra un diff entre dos archivos. Un solo archivo parche puede contener las diferencias para cualquier número de archivos, y git diff produce diffs para todos los archivos alterados en el repositorio en un solo parche.

Frecuentemente, verás la salida de git diff mostrando dos versiones del mismo archivo y la diferencia entre ellos.

Para demostrar, considera el estado en otra rama llamada diffs:

git checkout diffs

De nuevo, te animo a ejecutar los comandos conmigo - asegúrate de clonar el repositorio de

https://github.com/Omerr/gitting_things_repo.git

En el estado actual, el directorio activo es un repositorio de Git, con una estado limpio:

git_status_branch_diffs
git status

Toma un archivo existente, my_file.py:

An example file -
An example file - my_file.py

Y cambia la segunda línea de print('An example function!') a print('An example function! And it has been changed!'):

The contents of  after modifying the second line
The contents of my_file.py after modifying the second line

Guarda tus cambios, pero no lo pongas en el área de preparación o no lo confirmes. Luego, ejecuta git diff:

The output of  for  after changing it
The output of git diff for my_file.py after changing it

La salida de git diff muestra la diferencia entre las versiones de my_file.py en el área de preparación, el cual en este caso es el mismo que la última confirmación (HEAD), y la versión en el directorio de trabajo.

Cubrí los términos "directorio de trabajo", "área de preparación", y "confirmación" en el capítulo de objetos de Git, así que échale un vistazo en caso que te gustaría refrescar tu memoria. Como un recordatorio, los términos "área de preparación" e "índice" son intercambiables, y ambos son ampliamente usados.

At this state, the status of the working dir is different from the status of the index. The status of the index is the same as that of
At this state, the status of the working dir is different from the status of the index. The status of the index is the same as that of HEAD

Para ver la diferencia entre el directorio de trabajo y el área de preparación, usa git diff, sin ningún argumento adicional.

Without switches,  shows the difference between the staging area and the working directory
Without switches, git diff shows the difference between the staging area and the working directory

Como puedes ver, git diff aquí lista file A y file B apuntando a my_file.py. file A aquí se refiere a la versión de my_file.py en el área de preparación, donde file B se refiere a su versión en el directorio de trabajo.

Fíjate que si modificas my_file.py en un editor de texto, y no guardas el archivo, entonces git diff no estará al tanto de los cambios que has hecho. Esto se debe a que no han sido guardados en el directorio de trabajo.

Podemos proveer unos pocos cambios a git diff para obtener el diff entre el directorio de trabajo y una confirmación específica, o entre el área de preparación y la última confirmación, o entre dos confirmaciones, y así sucesivamente.

Primero crea un nuevo archivo, new_file.txt, y guárdalo:

A simple new file saved as new_file.txt
A simple new file saved as new_file.txt

Actualmente el archivo está en el directorio de trabajo, y actualmente está sin seguimiento en Git.

A new, untracked file
A new, untracked file

Ahora pónlo en el área de preparación y confirma este archivo:

git add new_file.txt
git commit -m "Commit 3"

Ahora, el estado de HEAD es el mismo que el estado del área de preparación, así también como el árbol de trabajo:

The state of HEAD is the same as the index and the working dir
The state of HEAD is the same as the index and the working dir

Luego, edita new_file.txt agregando una nueva línea al principio y otra nueva línea al final:

Modifying  by adding a line in the beginning and another in the end
Modifying new_file.txt by adding a line in the beginning and another in the end

Como resultado, el estado es como sigue:

After saving, the state in the working dir is different than that of the index or
After saving, the state in the working dir is different than that of the index or HEAD

Un lindo truco sería usar git add -p, el cual te permite dividir los cambios inclusive dentro de un archivo, y considerar cuáles te gustaría ponerlos en el área de preparación.

En este caso, agrega la primera línea al índice, pero no la última línea. Para hacer eso, puedes dividir el hunk usando s, luego acepta el primer hunk al área de preparación (usando y), y no la segunda parte (usando n).

Si no estás seguro qué significa cada letra, siempre puedes usar un ? y Git te dirá.

Using , you can stage only the first change
Using git add -p, you can stage only the first change

Así que ahora el estado en HEAD están sin ninguna de esas nuevas líneas. En el área de preparación tienes la primer línea pero no la última línea, y en el directorio de trabajo tienes las dos nuevas líneas.

The state after staging only the first line
The state after staging only the first line

Si usas git diff, ¿qué sucederá?

 shows the difference between the index and the working dir
git diff shows the difference between the index and the working dir

Bueno, como se dijo antes, obtienes el diff entre el área de preparación y el árbol de trabajo.

¿Qué sucede si quiere obtener el diff entre el HEAD y el área de preparación? Para eso, puedes usar git diff --cached:

 shows the difference between  and the index
git diff --cached shows the difference between HEAD and the index

¿Y qué pasa si quieres la diferencia entre el HEAD y el árbol de trabajo? Para eso puedes ejecutar git diff HEAD:

 shows the difference between  and the working dir
git diff HEAD shows the difference between HEAD and the working dir

Para resumir los diferentes cambios para git diff que hemos visto hasta ahora, aquí hay un diagrama:

Different switches for
Different switches for git diff

Como un recordatorio, al principio de este capítulo usaste git diff --no-index. Con el conmutador --no-index, puedes comparar dos archivos que no son parte del repositorio - o de cualquier área de preparación.

Ahora, confirma los cambios que tienes en el área de preparación:

git commit -m "Commit 4"

Para observar el diff entre esta confirmación y su confirmación padre, puedes ejecutar el siguiente comando:

git diff HEAD~1 HEAD
The output of
The output of git diff HEAD~1 HEAD

Por cierto, puedes omitir el 1 de arriba y escribir HEAD~, y obtener el mismo resultado. Usando 1 es la forma explícita para expresar que te refieres al primer padre de la confirmación.

Fíjate que escribir la confirmación padre aquí, HEAD~1, primero resulta en un diff mostrando cómo obtener de la confirmación padre a la confirmación actual. Por supuesto, podría también generar el diff en reversa al escribir:

git diff HEAD HEAD~1
The output of  generates the reverse patch
The output of git diff HEAD HEAD~1 generates the reverse patch

Para resumir todos los diferentes cambios para git diff que cubrimos en esta sección, mira este diagrama:

The different switches for
The different switches for git diff

Una forma corta de ver el diff entre una confirmación y su padre es usando git show, por ejemplo:

git show HEAD
git_show_HEAD
git show HEAD

Esto es lo mismo que escribir:

git diff HEAD~ HEAD

Ahora podemos actualizar nuestro diagrama:

 is used to show the difference between commits
git diff HEAD~ HEAD is used to show the difference between commits

Puedes volver a este diagrama como una referencia cuando sea necesario.

Como un recordatorio, las confirmaciones de Git son copias instantáneas - del directorio de trabajo completo del repositorio, en un cierto punto en el tiempo. A pesar de todo, a veces no es útil considerar a una confirmación como una copia instantánea completa, sino más bien por los cambios que esta confirmación específica introdujo. En otras palabras, por el diff entre una confirmación padre a la confirmación siguiente.

Como aprendiste en el capítulo de Objetos de Git, Git almacena las copias instantáneas enteros. El diff es generado dinámicamente de los datos de la copia instantánea - al comparar los árboles principales de la confirmación y su padre.

Por supuesto, Git puede comparar cualquiera de las dos copias instantáneas en el tiempo, no sólo confirmaciones adyacentes, y también generar un diff de archivos que no son incluidos en un repositorio.

Cómo aplicar Parches

Al usar git diff puedes ver un parche que Git genera, y luego puedes aplicar este patch usando git apply.

Nota Histórica

En realidad, compartir parches solía ser la principal forma de compartir código al comienzo del código abierto. Pero ahora - virtualmente todos los proyectos se han movido a compartir confirmaciones de Git directamente a través de pull requests (llamados "merge requests - peticiones de fusión" en algunas plataformas).

El mayor problema con usar parches es que es difícil de aplicar un parche cuando tu directorio de trabajo no coincide con la confirmación previa del emisor. Perder el historial de la confirmación lo vuelve difícil para resolver conflictos. Entenderás mejor esto a medida que indagues más profundamente en el proceso de git apply, especialmente en el próximo capítulo donde cubrimos las fusiones.

Un Simple Parche

¿Qué significa aplicar un parche? ¡Es tiempo de intentarlo!

Toma la salida de git diff:

git diff HEAD~1 HEAD

Y almacénalo en un archivo:

git diff HEAD~1 HEAD > my_patch.patch

Usa reset para deshacer la última confirmación:

git reset --hard HEAD~1

No te preocupes del último comando - te lo explicaré en detalle en la Parte 3, donde discutimos deshacer cambios. En breve, nos permite "resetear" el estado a donde HEAD está apuntando, así también como el estado del índice y del directorio de trabajo. En el ejemplo de arriba, todos están puestos al estado de HEAD~1, o "Confirmación 3" en el diagrama.

Así que después de ejecutar el comando reset, los contenidos del archivo son como sigue (el estado de "Confirmación 3"):

nano new_file.txt
nano_new_file-1
new_file.txt

Y aplicarás este parche que recién has guardado:

nano my_patch.patch
The patch you are about to apply, as generated by git diff
The patch you are about to apply, as generated by git diff

Este parche le dice a Git que encuentre las líneas:

This is a new file
With new content!

Esas líneas solían ser el número de línea 1 y número de línea 2 en new_file.txt, y agrega una línea con el contenido START! justo arriba de ellos.

Ejecuta este comando para aplicar el parche:

git apply my_patch.patch

Y como resultado, obtienes esta versión de tu archivo, justo como la confirmación que has creado antes:

nano new_file.txt
The contents of  after applying the patch
The contents of new_file.txt after applying the patch

Entendiendo la Líneas de Contexto

Para entender la importancia de las líneas de contexto, considera un escenario más avanzado. ¿Qué sucede si los números de líneas han cambiado desde que creaste el archivo parche?

Para probar, comienza por crear otro archivo:

nano test.text
Creating another file -
Creating another file - test.txt

Pónlo en el área de preparación y confirma este archivo:

git add test.txt

git commit -m "Test file"

Ahora, cambia este archivo al agregar una nueva línea, y también eliminando la línea antes de la última:

Changes to
Changes to test.txt

Observa la diferencia entre la versión original del archivo y la versión incluyendo tus cambios:

git diff -- test.txt
The output for git diff --
The output for git diff -- test.txt

(Usando --test.txt le dice a Git que ejecute el comando diff, tomando en consideración solamente a test.txt, así no obtienes el diff para otros archivos.)

Almacena este diff en un archivo parche:

git diff -- test.txt > new_patch.patch

Ahora, resetea tu estado a ese antes de introducir los cambios:

git reset --hard

Si llegaras a aplicar new_patch.patch ahora, simplemente funciona.

Ahora consideremos un caso más interesante. Modifica a test.txt nuevamente al agregar una nueva línea al principio:

Adding a new line at the beginning of
Adding a new line at the beginning of test.txt

Como resultado, los números de líneas son diferentes de la versión original donde el parche ha sido creado. Considera el parche que creaste antes:

new_patch
new_patch.patch

Asume que la línea With more text es la segunda línea en test.txt, el cual no es más el caso. Así que... ¿funcionará git apply?

git apply new_patch.patch

¡Funcionó!

Por defecto, Git busca las 3 líneas de contexto antes y después de cada cambio introducido en el parche - como puedes ver, son incluidos en el archivo parche. Si tomas tres líneas antes y después de la línea agregada, y tres líneas antes y después de la línea eliminada (en realidad solamente una línea después, ya que no existen otras líneas) - llegas al archivo parche. Si todas estas líneas existen - entonces aplicar el parche funciona, inclusive si los números de línea cambiaron.

Resetea el estado de nuevo:

git reset --hard

¿Qué sucede si cambias una de las líneas de contexto? Intenta cambiando la línea With more text a With more text!:

Changing the line  to
Changing the line `With more text` to With more text!

Y ahora:

git apply new_patch.patch
 doesn't apply the patch
git apply doesn't apply the patch

Bueno, no. El parche no se aplica. Si no estás seguro por qué, o solo quieres entender mejor el proceso que Git está realizando, puedes agregar el argumento --verbose a git apply, así:

git apply --verbose new_patch.patch
 shows the process Git is taking to apply the patch
git apply --verbose shows the process Git is taking to apply the patch

Parece que Git buscó líneas del archivo, incluyendo la línea "With more text", justo antes de la línea "It has some really nice lines". Esta secuencia de líneas ya no existen en el archivo. Ya que Git no puede encontrar esta secuencia, no puede aplicar el parche.

Como se mencionó antes, por defecto, Git busca 3 líneas de contexto antes y después de cada cambio introducido en el parche. Si las tres líneas circundantes no existen, Git no puede aplicar el parche.

Le puedes pedir a Git que se base en menos líneas de contexto, usando el argumento -C. Por ejemplo, para pedir a Git que busque 1 línea de contexto circundante, ejecuta el siguiente comando:

git apply -C1 new_patch.patch

¡El parche se aplica!

git_apply_c1
git apply -C1 new_patch.patch

¿Por qué es eso? Considera el parche nuevamente:

new_patch-1
new_patch.patch

Cuando se aplica el parche con la opción -C1, Git está buscando las líneas:

Like this one
And that one

para agregar la línea !!!This is the new line!!! entre estas dos líneas. Estas líneas existen (e, importantemente, aparecen justo después de la otra). Como resultado, Git puede agregar la línea entre ellos con éxito, aunque los números de línea cambiaron.

De manera similar, Git buscaría las líneas:

How wonderful
So we are writing an example
Git is awesoome!

Ya que Git puede encontrar estas líneas, Git puede eliminar el del medio.

Si cambiamos una de estas líneas, digamos, cambiamos "How wonderful" a "How very wondeful", entonces Git no sería capaz de encontrar la cadena de arriba, y así el parche no se aplicaría.

Recapitulando - Git Diff y Patch

En este capítulo, aprendiste qué es un diff, y la diferencia entre un diff y un parche. Aprendiste cómo generar varios parches usando diferentes conmutadores para git diff. También aprendiste cómo luce la salida de git diff, y cómo se construye. Por último, aprendiste cómo se aplican los patches, y específicamente la importancia del contexto.

Entender los diff es un hito importante para entender muchos otros procesos dentro de Git - por ejemplo, fusionando o rebasing, los cuales exploraremos en los próximos capítulos.

Capítulo 7 - Entendiendo la Fusión de Git

Al leer este capítulo, vas a entender realmente git merge, una de las operaciones más comunes que ejecutarás en tus repositorios de Git.

¿Qué es un Merge (Fusión) en Git?

Fusionar es el proceso de combinar los cambios recientes de varias ramas en una sola nueva confirmación. Esta confirmación apunta a estas ramas.

De una forma, fusionar es el complemento de ramificación en control de versiones: una rama te permite trabajar de manera simultánea con otros en un conjunto particular de archivos, donde una fusión te permite luego combinar trabajos separados en ramas que difieren de una confirmación padre en común.

Muy bien, vamos poco a poco.

Recuerda que en Git, una rama es sólo un nombre que apunta a una confirmación única. Cuando pensamos sobre las confirmaciones como si estuvieran sólo "en" una rama específica, en realidad son accesibles a través de la cadena principal desde la confirmación a la que la rama está apuntando.

Eso es, si consideras este gráfico de confirmación:

Commit graph with
Commit graph with feature_1

Ves la rama feature_1, el cual apunta a una confirmación con el valor de SHA-1 de ba0d2. Como en los capítulos previos, solamente escribo los primeros 5 dígitos del valor de SHA-1 para brevedad.

Fíjate que la confirmación 54a9d está también "en" esta rama, ya que es la confirmación antecesora de bad0d2. Si comienzas desde el puntero de feature_1, llegas a ba0d2, el cual luego apunta a 54a9d. Puedes continuar en la cadena de padres, y todas estas confirmaciones accesibles son considerados estar "en" el feature_1.

Cuando fusionas con Git, fusionas confirmaciones. Casi siempre, fusionamos dos confirmaciones al referirnos a ellos con los nombres de rama a los que apuntan. De esa forma decimos que "fusionamos ramas" - aunque por debajo, en realidad fusionamos confirmaciones.

Tiempo de manos a la obra

Para este capítulo, usaré el siguiente repositorio:

https://github.com/Omerr/gitting_things_merge.git

Como en capítulos previos, te animo a clonarlo localmente y tener el mismo punto de comienzo que estoy usando para este capítulo.

Muy bien, digamos que tenemos este simple repositorio aquí, con una rama llamada main, y unas pocas confirmaciones con los mensajes de confirmación de "Commit 1", "Commit 2", y "Commit 3":

A simple repository with three commits
A simple repository with three commits

Luego, crea una rama feature al escribir git branch new_feature:

Creating a new branch with
Creating a new branch with git branch

Y cambia a HEAD para que apunte a esta nueva rama, usando git checkout new_feature (o git switch new_feature). Puedes ver la salida usando git log:

The output of  after using
The output of git log after using git checkout new_feature

Como recordatorio, podrías también escribir git checkout -b new_feature, el cual crearía una nueva rama y cambiar el HEAD a que apunte a esta nueva rama.

Si necesitas un recordatorio sobre las ramas y cómo son implementados por debajo, por favor mira el capítulo 2. Sí, míralo. 😇

Ahora, en la rama new_feature, implementa una nueva característica. En este ejemplo, editaré un archivo que ya existe que luce así antes de editarlo:

 before editing it
code.py before editing it

Y ahora lo editaré para que incluya una nueva función:

Implementing
Implementing new_feature

Y afortunadamente, este no es un libro de programación, así que esta función es correcta 😇

Luego, colócalo en el área de preparación y confirma esta confirmación:

git add code.py

git commit -m "Commit 4"

Viendo al historial, tienes el branch new_feature, ahora apuntando a "Commit 4", el cual apunta a su antecesor, "Commit 3". La rama principal también está apuntando a "Commit 3".

The history after committing "Commit 4"
The history after committing "Commit 4"

¡Es tiempo de fusionar la nueva característica! Eso es, fusionar estas dos ramas, main y new_feature. O, en la jerga de Git, fusionar new_feature dentro de main. Esto significa fusionar "Commit 4" y "Commit 3". Esto es muy trivial, ya que después de todo, "Commit 3" es un ancestro de "Commit 4".

Verifica la rama principal (con git checkout main), y ejecuta la fusión usando git merge new_feature:

Merging  into
Merging new_feature into main

Ya que new_feature en realidad nunca difirió de main, Git podría realizar una fusión directa. Así que, ¿qué pasó aquí? Considera el historial:

The result of a fast-forward merge
The result of a fast-forward merge

Aunque usaste git merge, no hubo una fusión en sí aquí. En realidad, Git hizo algo muy sencillo - resetea la rama principal para que apunte a la misma confirmación como la rama new_feature.

En caso que no quieres que eso suceda, sino que quieres que Git realmente realice una fusión, podrías cambiar la configuración de Git, o ejecutar el comando de fusión con el argumento --no-ff.

Primero, deshace la última confirmación:

git reset --hard HEAD~1

Recordatorio: si esta forma de usar reset no te queda claro, no te preocupes - los cubriremos en detalle en la Parte 3. No es crucial para esta introducción de merge. Por ahora, es importante entender que básicamente deshace la operación de fusión.

Sólo para clarificar, ahora si verificas a new_feature nuevamente:

git checkout new_feature

El historial luciría igual como antes de la fusión:

The history after using
The history after using git reset --hard HEAD~1

Luego, realiza la fusión con el argumento --no-fast-forward (en corto --no-ff):

git checkout main
git merge new_feature --no-ff

Ahora, si miramos el historial usando git lol:

History after merging with the  flag
History after merging with the --no-ff flag

(Recordatorio: git lol es un alias que agregué a Git para ver visiblemente el historial de una manera gráfica. Lo puedes encontrar, junto con los otros componentes de mi configuración, en la parte de Mi Configuración del capítulo de Introducción.)

Considera este historial, puedes ver que Git creó una nueva confirmación, una confirmación de fusión.

Si consideras esta confirmación un poco más de cerca:

git log -n1
The merge commit has two parents
The merge commit has two parents

Verás que esta confirmación en realidad tiene dos padres - "Commit 4", el cual fue la confirmación a la que new_feature apuntó cuando ejecutaste git merge, y "Commit 3", el cual fue la confirmación a la que main apuntó.

Una confirmación de fusión tiene dos padres: las dos confirmaciones que fusionó.

La confirmación de fusión nos muestra el concepto de fusión bastante bien. Git toma dos confirmaciones, usualmente referenciados por dos ramas diferentes, y los fusiona juntos.

Después de la fusión, comenzaste el proceso desde main, estás todavía en main, y el historial de new_feature ha sido fusionado dentro de esta rama. Ya que comenzaste con main, luego "Commit 3", a la que apuntó main, es el primer antecesor de la confirmación de fusión, mientras que "Commit 4", la cual la fusionaste dentro de main, es el segundo antecesor de la confirmación de fusión.

Fíjate que empezaste en main cuando éste apuntaba a "Commit 3", y Git te ayudó bastante. Cambió el árbol de trabajo, el índice, y también el HEAD y creó un nuevo objeto de confirmación. Al menos cuando usas git merge sin el argumento --no-commit y cuando no es una fusión directa, Git hace todo eso.

Esto fue un caso super sencillo, donde las ramas que fusionaste no difirieron para nada. Pronto consideraremos casos más interesantes.

A propósito, puedes usar git merge para fusionar más de dos confirmaciones - en realidad, cualquier número de confirmaciones. Esto se hace raras veces, y para adherirnos al principio de practicidad de este libro, no lo profundizaremos.

Otra forma de ver a git merge es uniendo dos o más historiales de desarrollo juntos. Eso es, cuando fusionas, incorporas cambios desde las confirmaciones nombradas, desde el momento en que sus historiales difirieron de la rama actual, en la rama actual. Usé el término "rama" aquí, pero estoy enfatizando esto nuevamente - en realidad estamos fusionando confirmaciones.

Es tiempo de un Caso más Avanzado

Es tiempo de considerar un caso más avanzado, lo cual es probablemente el caso más común donde usamos git merge explícitamente - donde necesitas fusionar ramas que sí difieren uno del otro.

Supón que tenemos dos personas trabajando en este repo ahora, John y Paul.

John creó una rama:

git checkout -b john_branch
A new branch,
A new branch, john_branch

Y John ha escrito una nueva canción en un nuevo archivo, lucy_in_the_sky_with_diamonds.md. Bueno, creo que John Lennon realmente no escribió en formato Markdown, o no usó Git para ese asunto, pero pretendamos que lo hizo para esta explicación.

git add lucy_in_the_sky_with_diamonds.md
git commit -m "Commit 5"

Mientras John estaba trabajando en esta canción, Paul también estaba escribiendo, en otra rama. Paul había comenzado desde main:

git checkout main

Y creó su propia rama:

git checkout -b paul_branch

Y Paul escribió su canción en un archivo llamado penny_lane.md. Paul puso en el área de preparación y confirmó este archivo:

git add penny_lane.md
git commit -m "Commit 6"

Así que ahora nuestro historial luce así - donde tenemos dos ramas distintas, que se ramifican desde main, con historiales distintos:

The history after John and Paul committed
The history after John and Paul committed

John está feliz con su rama (eso es, su canción), así que él decide fusionarlo en la rama main:

git checkout main
git merge john_branch

En realidad, este es una fusión directa, como hemos aprendido antes. Puedes validar eso al ver el historial (usando git lol, por ejemplo):

Merging  into  results in a fast-forward merge
Merging john_branch into main results in a fast-forward merge

A este punto, Paul también quiere fusionar su rama dentro de main, pero ahora una fusión directa ya no es relevante - hay dos diferentes historiales aquí: el historial de main y el de paul_branch. No es que paul_branch solamente agrega confirmaciones encima de la rama main o vice versa.

Ahora las cosas se ponen interesante. 😎😎

Primero, dejemos a Git que haga el trabajo duro por ti. Después de eso, entenderemos lo que en realidad está sucediendo por debajo.

git merge paul_branch

Considera este historial ahora:

When you merge , you get a new merge commit\label{fig-history-after-git-merge}
When you merge paul_branch, you get a new merge commit

Lo que tienes es una nueva confirmación, con dos antecesores - "Commit 5" y "Commit 6".

En el directorio de trabajo, puedes ver que la canción de John así también como la canción de Paul están ahí (si usas ls, verás ambos archivos en el directorio de trabajo).

Bien, Git realmente fusionó los cambios por ti. ¿Pero cómo hace que eso suceda?

Deshace la última confirmación:

git reset --hard HEAD~

Cómo realizar una Fusión de Tres Vías en Git

Es tiempo de entender lo que realmente está pasando por debajo. 😎

Lo que Git ha hecho aquí se le llama una fusión de 3 vías. Al delinear el proceso de una fusión de 3 vías, usaré el término "rama" para simplicidad, pero deberías recordar que podrías también fusionar dos (o más) confirmaciones que no son referenciados por una rama.

El proceso de fusión de 3 vías incluye estas etapas:

Primero, Git localiza el ancestro común de las dos ramas. Eso es, la confirmación común desde el cual las ramas de fusión difirieron más recientemente. Técnicamente, esto en realidad es la primer confirmación que es accesible desde ambas ramas. A esta confirmación entonces se le llama la base de fusión.

Segundo, Git calcula dos diffs - un diff de la base de fusión a la primer rama, y otro diff desde la base de fusión a la segunda rama. Git genera parches basados en esos diffs.

Tercero, Git aplica ambos parches a la base de fusión usando un algoritmo de fusión de 3 vías. El resultado es el estado de la nueva confirmación de fusión.

The three steps of the 3-way merge algorithm: (1) locate the common ancestor; (2) calculate diffs from the merge base to the first branch, and from the merge base to the second branch; (3) apply both patches together
The three steps of the 3-way merge algorithm: (1) locate the common ancestor (2) calculate diffs from the merge base to the first branch, and from the merge base to the second branch (3) apply both patches together

Así que, devuelta a nuestro ejemplo.

En el primer paso, Git mira desde ambas ramas - main y paul_branch - y atraviesa el historial para encontrar la primera confirmación que es accesible desde ambos. En este caso, este sería.. ¿cuál confirmación?

Correcto, la confirmación de fusión (el que tiene a "Commit 3" y "Commit 4" como sus predecesores).

Si no estás seguro, siempre puedes preguntarle a Git directamente:

git merge-base main paul_branch
The merge base is the merge commit with "Commit 3" and "Commit 4" as its parents. Note: the previous commit merge is blurred as it is not reachable via the current history following the  command
The merge base is the merge commit with "Commit 3" and "Commit 4" as its parents. Note: the previous commit merge is blurred as it is not reachable via the current history following the reset command

A propósito, este es el caso más común y sencillo, donde tenemos una única elección obvia para la base de fusión. En casos más complicados, podría haber múltiples posibilidades para una base de fusión, pero esto no está dentro de nuestro enfoque.

En el segundo paso, Git calcula las diferencias. Así que primero calcula la diferencia entre la confirmación de fusión y "Commit 5":

git diff 4f90a62 4683aef

(Los valores de SHA-1 serán distintas en tu máquina.)

The diff between the merge commit and "Commit 5"\label{fig-john-patch}
The diff between the merge commit and "Commit 5"

Si no te sientes cómodo con la salida de git diff, puedes leer el capítulo anterior donde lo describí en detalle.

Puedes almacenar esa diferencia en un archivo:

git diff 4f90a62 4683aef > john_branch_diff.patch

Luego, Git calcula la diferencia entre la confirmación de fusión y "Commit 6":

git diff 4f90a62 c5e4951
The diff between the merge commit and "Commit 6"
The diff between the merge commit and "Commit 6"

Escribe esto en un archivo también:

git diff 4f90a62 c5e4951 > paul_branch_diff.patch

Ahora Git aplica esos parches en la base de fusión.

Primero, intenta eso directamente - aplica los parches (te lo explicaré en un momento). Esto no es lo que realmente hace Git por defecto, pero te ayudará a ganar un mejor entendimiento de por qué Git necesita hacer algo distinto.

Verifica la base de fusión primero, eso es, la confirmación de fusión:

git checkout 4f90a62

Y aplica el parche de John primero (como recordatorio, este es el parche que se muestra en la imagen con el subtítulo "The diff between the merge commit and "Commit 5""):

git apply --index john_branch_diff.patch

Fíjate que por ahora no hay una confirmación de fusión. git apply actualiza el directorio de trabajo así también como el índice, ya que usamos el conmutador --index.

Puedes observar el estado usando git status:

Applying John's patch on the merge commit
Applying John's patch on the merge commit

Así que ahora la nueva canción de John se incorpora en el índice. Aplica el otro parche:

git apply --index paul_branch_diff.patch

Como resultado, el índice contiene los cambios desde ambas ramas.

Ahora es tiempo de confirmar tu fusión. Ya que el comando de porcelana git commit siempre genera una confirmación con un solo predecesor, necesitarías el comando de plomería subyacente - git commit-tree.

Si necesitas un recordatorio sobre los comandos de porcelana vs de plomería, mira el capítulo 4 donde expliqué estos términos, y creé un repo entero desde cero.

Recuerda que cada objeto de confirmación de Git apunta a un sólo árbol. Así que necesitas registrar los contenidos del índice en un árbol:

git write-tree

Ahora obtienes el valor de SHA-1 del árbol creado, y puedes crear un objeto de confirmación usando git commit-tree:

git commit-tree <TREE_SHA> -p <COMMIT_5> -p <COMMIT_6> -m "Merge commit!"
Creating a merge commit
Creating a merge commit

Genial, ¡así que has creado un objeto de confirmación!

Recuerda que git merge también cambia el HEAD para que apunte al nuevo objeto de confirmación de fusión. Así que puedes simplemente hacer lo mismo:

git reset --hard db315a

Si miras al historial ahora:

The history after creating a merge commit and resetting
The history after creating a merge commit and resetting HEAD

(Nota: en este estado, HEAD está "separado" - eso es, directamente apunta a un objeto de confirmación en vez de una referencia nombrada. gg no muestra el HEAD cuando está "separado", así que no te confundas si no puedes ver el HEAD en la salida de gg.)

Esto es casi lo que queríamos. Recuerda que cuando ejecutaste git merge, el resultado fue HEAD apuntando a main el cual apuntó a la nueva confirmación creada (como se muestra en la imagen con el subtítulo "When you merge paul_branch), obtienes una nueva confirmación de fusión". ¿Qué deberías hacer entonces?

Bueno, lo que quieres es modificar a main, así que lo puedes apuntar a la nueva confirmación:

git checkout main
git reset --hard db315a

Y ahora tienes el mismo resultado cuando ejecutaste git merge: main apunta a la nueva confirmación, el cual tiene "Commit 5" y Commit 6" como sus predecesores. Puedes usar git lol para verificar eso.

Así que esto es exactamente el mismo resultado como la fusión hecha por Git, con la excepción de la marca de tiempo y así el valor de SHA-1, por supuesto.

Sobretodo, tienes que fusionar los contenidos de las dos confirmaciones - eso es, el estado de los archivos, y también el historial de esas confirmaciones - creando una confirmación de fusión que apunta a ambos historiales.

En este caso sencillo, podrías en realidad aplicar los parches usando git apply y todo funciona bastante bien.

Rápida recapitulación de una Fusión de tres vías

Así que para recapitular rápidamente, en una fusión de tres vías, Git:

  • Primero, localiza la base de fusión - el ancestro común de las dos ramas. Eso es, la primer confirmación que es accesible de ambas ramas.
  • Segundo, Git calcula los dos diffs - un diff de la base de fusión a la primera rama, y otro diff de la base de fusión a la segunda rama.
  • Tercero, Git aplica ambos parches a la base de fusión, usando un algoritmo de fusión de tres vías. No he explicado la fusión de tres vías todavía, pero lo explicaré más tarde. El resultado es el estado de la nueva confirmación de fusión.

También puedes entender por qué se llama una "fusión de 3 vías": Git fusiona, tres estados diferentes - el de la primera rama, el de la segunda rama, y su ancestro en común. En nuestro ejemplo anterior, main, paul_branch, y la confirmación de fusión (con "Commit 3" y "Commit 4" como predecesores), respectivamente.

Esto es improbable, digamos, los ejemplos directos que vimos antes. Los ejemplos directos son en realidad un caso de una fusión de dos vías, ya que Git solamente compara dos estados - por ejemplo, a donde main apuntó, y a donde john_branch apuntó.

Continuando

Todavía, esto fue un sencillo caso de una fusión de 3 vías. John y Paul crearon canciones distintos, así que cada uno de ellos tocaron un archivo distinto. Fue bastante directo para ejecutar la fusión.

¿Qué hay sobre casos más interesantes?

Digamos que ahora John y Paul son co-autores de una nueva canción.

Así que, John verificó la rama main y comenzó a escribir la canción:

git checkout main
John's new song
John's new song

Él lo puso en el área de preparación y lo confirmó ("Commit 7"):

git add a_day_in_the_life.md
git commit -m "Commit 7"
John's new song is committed
John's new song is committed

Ahora, Paul hace una rama:

git checkout -b paul_branch_2

Y edita la canción, agregando otro verso:

Paul added a new verse
Paul added a new verse

Por supuesto, la canción original no incluye el título "Paul's Verse", pero lo agregué para claridad.

Paul pone en el área de preparación y confirma los cambios:

git add a_day_in_the_life.md
git commit -m "Commit 8"
The history after introducing "Commit 8"
The history after introducing "Commit 8"

John también hace una rama tomando de main y agrega unas dos líneas adicionales al final:

git checkout main
git checkout -b john_branch_2
John added the two last lines
John added the two last lines

John pone en el área de preparación y confirma sus cambios también ("Commit 9"):

git add a_day_in_the_life.md
git commit -m "Commit 9"

Este es el historial resultante:

The history after John's last commit
The history after John's last commit

Así que, Paul y John modificó el mismo archivo en diferentes ramas. ¿Tendrá éxito Git al fusionarlos?

Digamos que ahora no vamos por main, sino que John intentará fusionar la nueva rama de Paul en su rama:

git merge paul_branch_2

¡Espera! ¡No ejecutes este comando! ¿Por qué le dejarías a Git hacer todo el trabajo duro? Estás intentando de entender el proceso aquí.

Así que, primero, Git necesita encontrar la base de fusión. ¿Puedes ver cual confirmación sería?

Correcto, sería la última confirmación en la rama main, donde los dos difieren - eso es, "Commit 7".

Puedes verificar eso usando:

git merge-base john_branch_2 paul_branch_2
"Commit 7" is the merge base
"Commit 7" is the merge base

Cambia a la base de fusión así más tarde puedes aplicar los parches que crearás:

git checkout main

Genial, ahora Git debería calcular los diffs y generar los parches. Puedes observar los diffs directamente:

git diff main paul_branch_2
The output of
The output of git diff main paul_branch_2

¿Tendrá éxito en aplicar este parche? Bueno, sin problema, Git tiene todas las líneas de contexto en lugar.

Cambia a la base de fusión (el cual es "Commit 7", también referenciado por main), y pide a Git que aplique este parche:

git checkout main
git diff main paul_branch_2 > paul_branch_2.patch
git apply --index paul_branch_2.patch

Y esto funcionó, sin problemas para nada.

Ahora, calcula la diff entre la nueva rama de John y la base de fusión. Fíjate que no has confirmado los cambios aplicados, así que john_branch_2 todavía apunta a la misma confirmación como antes, "Commit 9":

git diff main john_branch_2
The output of
The output of git diff main john_branch_2

¿Funcionará si se aplica este diff?

Bueno, de hecho, sí. Fíjate que aunque los números de líneas han cambiado en la versión actual del archivo, gracias a las líneas de contexto que Git es capaz de localizar donde necesita agregar estas líneas...

Git can rely on the context lines
Git can rely on the context lines

Guarda este parche y luego aplícalo:

git diff main john_branch_2 > john_branch_2.patch
git apply --index john_branch_2.patch

Observa el archivo resultante:

The result after applying Paul's patch
The result after applying Paul's patch

Genial, exactamente lo que queríamos.

Ahora puedes crear el árbol y la confirmación relevante:

git write-tree

No te olvides de especificar ambos predesores:

git commit-tree <TREE-ID> -p paul_branch_2 -p john_branch_2 -m "Merging new changes"

¿Viste cómo usé los nombres de rama aquí? Después de todo, sólo son punteros a las confirmaciones que queremos.

Genial, mira el log de la nueva confirmación:

 after creating the merge commit
git lol <SHA_OF_THE_MERGE_COMMIT> after creating the merge commit
The history after creating the merge commit
The history after creating the merge commit

Exactamente lo que queríamos.

También puedes dejar a Git que haga el trabajo para ti. Puedes cambiar a john_branch_2, el cual no has movido - así que todavía apunta a la misma confirmación como lo hizo antes de la fusión. Así que todo lo que necesitas es ejecutar:

git checkout john_branch_2
git merge paul_branch_2

Observa el historial resultante:

 after letting Git perform the merge
git lol after letting Git perform the merge
A visualization of the history after letting Git perform the merge
A visualization of the history after letting Git perform the merge

Como antes, tienes una confirmación de fusión apuntando a "Commit 8" y "Commit 9" como sus predecesores. "Commit 9" es el primer predecesor ya que lo fusionaste.

Pero esto fue bastante sencillo... John y Paul trabajaron en el mismo archivo, pero en partes muy distintas. También podrías directamente aplicar los cambios de Paul a la rama de John. Si vuelves a la rama de John antes de la fusión:

git reset --hard HEAD~

Y ahora aplica los cambios de Paul:

git apply --index paul_branch_2.patch

Obtendrías el mismo resultado.

Pero, ¿qué sucede cuando las dos ramas incluyen cambios en los mismos archivos, en las mismas ubicaciones?

Casos de Fusión de Git más avanzados

¿Qué sucedería si John y Paul fueran a coordinar una nueva canción, y lo realizan juntos?

En este caso, John crea la primer versión de esta canción en la rama main:

git checkout main
nano everyone.md
The contents of  prior to the first commit
The contents of everyone.md prior to the first commit

A propósito, este texto es de hecho tomado de la versión que John Lennon grabó para una demo en 1968. Pero esto no es un libro sobre los Beatles. Si estás interesado sobre el proceso por el que los Beatles atravesaron mientras escribían esta canción, puedes seguir los enlaces al final de este capítulo.

git add everyone.md
git commit -m "Commit 10"
Introducing "Commit 10"
Introducing "Commit 10"

Ahora John y Paul se separon. Paul crea un nuevo verso al principio:

git checkout -b paul_branch_3
nano everyone.md
Paul added a new verse in the beginning
Paul added a new verse in the beginning

También, mientras hablando con John, decidieron cambiar la palabra "feet" a "foot", así que Paul agrega este cambio también.

Y Paul agrega y confirma sus cambios al repo:

git add everyone.md
git commit -m "Commit 11"
The history after introducing "Commit 11"
The history after introducing "Commit 11"

Puedes observar los cambios de Paul, comparando este estado de la rama al estado de la rama main:

git diff main
The output of  from Paul's branch
The output of git diff main from Paul's branch

Almacena este diff en un archivo parche:

git diff main > paul_3.patch

Ahora devuelta a main...

git checkout main

John decide hacer otro cambio, en su propia nueva rama:

git checkout -b john_branch_3

Y reemplaza la línea "Everyone had the boot in" con la línea "Everyone had a wet dream". Además, John cambió la plabra "feet" a "foot", siguiendo su charla con Paul.

Observa el diff:

git diff main
The output of  from John's branch
The output of git diff main from John's branch

Almacena esta salida también:

git diff main > john_3.patch

Ahora, pónlo en el área de preparación y confirma:

git add everyone.md
git commit -m "Commit 12"

Este debería ser tu historial actual:

The history after introducing "Commit 12"
The history after introducing "Commit 12"

Fíjate que eliminé john_branch_2 y paul_branch_2 para simplicidad. Por supuesto, puedes eliminarlos de Git usando git branch -D <branch_name>. Como resultado, estos nombres de ramas no aparecerán en la salida de git log u otros comandos similares.

Esto también se aplica a las confirmaciones que ya no son accesibles desde ninguna referencia nombrada, tales como "Commit 8" o "Commit 9". Ya que ya no son accesibles desde ninguna referencia nombrada por medio de la cadena de los antecesores, no serán incluidos en la salida de comandos tales como git log.

Devuelta a nuestra historia - Paul y John han agregado un nuevo verso, así que a John le gustaría fusionar los cambios de Paul.

¿Puede John simplemente aplicar el parche de Paul?

Considera el parche de nuevo:

git diff main paul_branch_3
The output of
The output of git diff main paul_branch_3

Como puedes ver, este diff se basa en la línea "Everyone had the boot in", pero esta línea ya no existe en la rama de John. Como resultado, podrías esperar que aplicar el parche falle. Continúa, inténtalo:

git apply paul_3.patch
Applying the patch failed
Applying the patch failed

De hecho, puedes ver que falló.

¿Pero debería fallar realmente?

Como expliqué anteriormente, git merge usa un algoritmo de 3 vías, y esto puede ser útil aquí. ¿Cuál sería el primer paso de este algoritmo?

Bueno, primero, Git encontraría la base de fusión - eso es, el ancestro común de la rama de Paul y de la rama de John. Considera el historial:

The history after introducing "Commit 12"
The history after introducing "Commit 12"

Así que el ancestro común de "Commit 11" y Commit 12" es "Commit 10". Puedes verificarlo ejecutando el comando:

git merge-base john_branch_3 paul_branch_3

Ahora podemos tomar los parches que generamos desde los diffs en ambas ramas, y aplicarlos al main. ¿Funcionaría?

Primero, intenta aplicar el parche de John, y luego el parche de Paul.

Considera el diff:

git diff main john_branch_3
The output of
The output of git diff main john_branch_3

Podemos almacenarlo en un archivo:

git diff main john_branch_3 > john_3.patch

Y aplicar este parche en el main:

git checkout main
git apply john_3.patch

Consideremos el resultado:

nano everyone.md
The contents of  after applying John's patch
The contents of everyone.md after applying John's patch

La línea cambió como se esperaba. Bien 😎

Ahora, ¿puede Git aplicar el parche de Paul? Para recordarte, este es el parche. Bueno, Git no puede aplicar este parche, porque este parche asume que la línea "Everyone had the boot in" existe. Tratar de aplicarlo es probable que falle:

The contents of Paul's patch
The contents of Paul's patch

Bueno, Git no puede aplicar este parche, porque este parche asume que la línea "Everyone had the boot in" existe. Tratar de aplicarlo es probable que falle:

git apply -v paul_3.branch
Applying Paul's patch failed
Applying Paul's patch failed

Lo que intentaste de hacer ahora, aplicar el parche de Paul en la rama main después de aplicar el parche de John, es lo mismo que estar en john_branch_3, e intentar de aplicar el parche. Eso es, ejecutando:

git apply paul_3.patch

¿Qué sucedería si intentaramos al revés?

Primero, limpia el estado:

git reset --hard

Y comienza desde la rama de Paul:

git checkout paul_branch_3

¿Podemos aplicar el parche de Paul? Como recordatorio, este es el estado de everyone.md en esta rama:

The contents of  on
The contents of everyone.md on paul_branch_3

Y este es el parche de John:

The contents of John's patch
The contents of John's patch

¿Funcionaría aplicar el parche de John?

Intenta contestarte a tí mismo antes de continuar leyendo.

Puedes intentar:

git apply john_3.patch
Git fails to apply John's patch
Git fails to apply John's patch

Bueno, ¡no! De nuevo, si estás seguro de lo que pasó, siempre puedes pedir con git apply para que sea un poco más verboso:

git apply -v john_3.patch
You can get more information by using the  flag
You can get more information by using the -v flag

Git está buscando "Everyone put the feet down", pero Paul ya ha cambiado esta línea así que ahora consiste de la palabra "foot" en vez de "feet". Como resultado, aplicar este parche falla.

Fíjate que cambiar el número de las líneas de contexto aquí (eso es, usando git apply con el argumento -C, como se discutió en el capítulo anterior) es irrelevante - Git es incapaz de localizar la línea actual que el parche está intentando de eliminar.

Pero en realidad, Git puede hacer que esto funcione, si sólo agregas un argumento para aplicar, diciéndole que haga una fusión de tres vías por debajo:

git apply -3 john_3.patch
Applying with  flag succeeds
Applying with -3 flag succeeds

Y considera el resultado:

The contents of  after the merge
The contents of everyone.md after the merge

¡Exactamente lo que queríamos! ¡Tienes el verso de Paul, y los de John!

Así que, ¿cómo Git fue capaz de lograr eso?

Bueno, como mencioné, Git realmente hizo una fusión de 3 vías, y con este ejemplo, será un buen momento para indagar lo que esto realmente significa.

Cómo funciona el Algoritmo de Fusión de 3 vías de Git

Vuelve al estado antes de aplicar este parche:

git reset --hard

Ahora tienes tres versiones: la base de fusión, el cual es "Commit 10", la rama de Paul, y la rama de John. En términos generales, podemos decir que estos son el merge base, commit A y commit B. Fíjate que el merge base es por definición un ancestro de ambos commit A y commit B.

Para realizar una fusión, Git mira al diff entre las tres diferentes versiones del archivo en cuestión de estas tres revisiones. En tu caso, es el archivo everyone.md, y las revisiones son "Commit 10", la rama de Paul - eso es, "Commit 11", y la rama de John, eso es, "Commit 12".

Git hace la decisión de fusión basado en el estado de cada línea en cada una de estas versiones.

The three versions considered for the 3-way merge
The three versions considered for the 3-way merge

En caso que no todas las tres versiones coincidan, eso es un conflicto. Git puede resolver muchos de estos conflictos automáticamente, como veremos ahora.

Consideremos líneas específicas.

Las primeras líneas aquí existen solamente en la rama de Paul:

Lines that appear on Paul's branch only
Lines that appear on Paul's branch only

Esto significa que el estado de la rama de John es igual al estado de la base de fusión. Así que la fusión de 3 vías va a la par con la versión de Paul.

En general, si el estado de la base de fusión es el mismo que el A, el algoritmo va la par con B. La razón se debe a que la base de fusión es el ancestro de A y de B, Git asume que esta línea no ha cambiado en A, y ha cambiado en B, el cual es la versión más reciente para esa línea, y por lo tanto debería ser tomado en cuenta.

If the state of the merge base is the same as , and this state is different from , the algorithm goes with
If the state of the merge base is the same as A, and this state is different from B, the algorithm goes with B

Luego, puedes ver líneas donde toda las tres versiones están de acuerdo - están en la base de fusión, A y B, con datos iguales.

Lines where all three versions agree
Lines where all three versions agree

En este caso el algoritmo tiene una opción trivial - sólo toma esa versión.

In case all three versions agree, the algorithm goes with that single version
In case all three versions agree, the algorithm goes with that single version

En un ejemplo previo, vimos que si la base de fusión y A están de acuerdo, y la versión de B es diferente, el agoritmo toma B. Esto funciona en la otra dirección también - por ejemplo, aquí tienes una línea que existe en la rama de John, distinto al de la base de fusión y al de la rama de Paul.

A line where Paul's version matches the merge base's version, and John has a different version
A line where Paul's version matches the merge base's version, and John has a different version

Por lo tanto, la versión de John es elegida:

If the state of the merge base is the same as , and this state is different from , the algorithm goes with
If the state of the merge base is the same as B, and this state is different from A, the algorithm goes with A

Ahora considera otro caso, donde A y B están de acuerdo en una línea, pero el valor en el que están de acuerdo es distinta del de la base de fusión: ambos John y Paul acordaron en cambiar la línea "Everyone put their feet down" a "Everyone put their foot down":

A line where Paul's version matches John's version; yet the merge base has a different version
A line where Paul's version matches John's version, yet the merge base has a different version

En este caso, el algoritmo toma la versión de A y de B.

In case A and B agree on a version which is different from the merge base's version, the algorithm picks the version on both A and B
In case A and B agree on a version which is different from the merge base's version, the algorithm picks the version on both A and B

Fíjate que esto no es un voto democrático. En el caso previo, el algoritmo tomó la versión minoritaria, ya que se parecía a la versión más reciente de esta línea. En este caso, llega a tomar la mayoría - pero solamente porque A y B son las revisiones que acordan en la nueva versión.

Lo mismo sucedería si usaramos git merge:

git merge john_branch_3

Sin especificar ningún argumento, git merge por defecto usará una fusión de 3 vías.

By default,  uses a 3-way merge algorithm
By default, git merge uses a 3-way merge algorithm

El estado de everyone.md después de ejecutar git merge john_branch sería el mismo que el resultado que alcanzaste al aplicar los parches con git apply -3.

Si consideras el historial:

Git's history after performing the merge
Git's history after performing the merge

Verás que la confirmación de fusión tiene dos antecesores: el primero es el "Commit 11", eso es, a donde apuntó paul_branch_3 antes de la fusión. El segundo es "Commit 12", a donde john_branch_3 apuntó, y a donde todavía apunta ahora.

¿Qué sucederá si ahora fusionas desde main? Eso es, cambiar a la rama main, el cual apunta al "Commit 10":

git checkout main

¿Y luego fusionar la rama de Paul?

git merge paul_branch_3

De hecho, obtenemos una fusión directa - como antes de ejecutar este comando, main era un ancestro de paul_branch_3.

A fast-forward merge
A fast-forward merge

Así que, esto es una fusión de 3 vías. En general, si todas las versiones acuerdan en una línea, luego esta línea es usada. Si A y la base de fusión coinciden, y B tiene otra versión, B es tomado. En el caso opuesto, donde la base de fusión y B coinciden, la versión A es seleccionado. Si A y B coinciden, esta versión es tomada, sea que la base de fusión esté de acuerdo o no.

Esta descripción deja una pregunta abierta: ¿qué sucede en casos donde todas las tres versiones están en desacuerdo?

Bueno, eso es un conflicto que Git no lo resuelve automáticamente. En estos casos, Git pide ayuda de un humano.

Cómo resolver conflictos de fusión

Siguiendo hasta ahora, deberías entender las bases del comando git merge, y cómo Git puede resolver automáticamente algunos conflictos. También entiendes qué casos son resueltos automáticamente.

Ahora, consideremos un caso más avanzado.

Digamos que Paul y John continúan trabajando en esta canción.

Paul crea una nueva rama:

git checkout -b paul_branch_4

Y él decide agregar algunos "Yeah" a la canción, así que él cambia este verso como sigue:

Paul's additions
Paul's additions

Así que Paul pone en el área de preparación y confirma estos cambios:

git add everyone.md
git commit -m "Commit 13"

Paul también crea otra canción, let_it_be.md y lo agrega al repo:

git add let_it_be.md
git commit -m "Commit 14"

Este es el historial:

The history after Paul introduced "Commit 14"
The history after Paul introduced "Commit 14"

Volviendo a main:

git checkout main

John también crea una rama:

git checkout -b john_branch_4

Y John también trabaja en la canción "Everyone had a hard year", luego para ser llamado "I've got a feeling" (de nuevo, esto no es un libro sobre los Beatles, así que no daré mas detalles aquí. Mira los enlaces adicionales si tienes curiosidad).

John decide cambiar todas las ocurrencias de "Everyone" to "Everybody":

John changes all occurrences of "Everyone" to "Everybody"
John changes all occurrences of "Everyone" to "Everybody"

Él pone en el área de preparación y confirma esta canción al repo:

git add everyone.md
git commit -m "Commit 15"

Bien. Ahora John también crea otra canción, across_the_universe.md. Él lo agrega al repo también:

git add across_the_universe.md
git commit -m "Commit 16"

Observa el historial nuevamente:

The history after John introduced "Commit 16"
The history after John introduced "Commit 16"

Puedes ver que el historial difiere de main, a dos ramas distintas - paul_branch_4, y john_branch_4.

A este punto, a John le gustaría fusionar los cambios introducidos por Paul.

¿Qué va a pasar aquí?

Recuerda los cambios introducidos por Paul:

git diff main paul_branch_4
The output of
The output of git diff main paul_branch_4

¿Qué piensas? ¿La fusión funcionará?

Inténtalo:

git merge paul_branch_4
A merge conflict
A merge conflict

¡Tenemos un conflicto!

Git no puede fusionar estas ramas por sí mismo. Puedes obtener una vista general del estado de fusión, usando git status:

The output of  right after the merge operation
The output of git status right after the merge operation

Los cambios que Git no tuvo problema resolviendo están en el área de preparación para confimar. Y hay una sección separada para "rutas no fusionadas" - estos son archivos con conflictos que Git no podría resolver por sí mismo.

Es tiempo de entender por qué y cuando estos conflictos suceden, cómo resolverlos, y también cómo Git los maneja por debajo.

¡Muy bien entonces! Espero que al menos estés emocionado como yo. 😇

Recordemos lo que sabemos sobre fusiones de 3 vías:

Primero, Git buscará la base de fusión - el ancestro común de john_branch_4 y paul_branch_4. ¿Cuál confirmación sería ese?

Sería la punta de la rama main, la confirmación en el cual fusionamos john_branch_3 en paul_branch_3.

De vuelta, si no estás seguro, puedes verificar eso ejecutando:

git merge-base john_branch_4 paul_branch_4

Y en el estado actual, git status sabe qué archivos están en el área de preparación y cuáles no.

Considera el proceso para cada archivo, el cual es el mismo que el algoritmo de fusión de 3 vías que consideramos por línea, pero a nivel de archivo:

across_the_universe.md existe en la rama de John, pero no existe en la base de fusión o en la rama de Paul. Así que Git elige incluir este archivo. Ya que ya estás en la rama de John y este archivo es incluido en la punta de esta rama, no es mencionado por git status.

let_it_be.md existe en la rama de Paul, pero no existe en la base de fusión o en la rama de John. Así que git merge "elige" incluirlo.

¿Qué hay sobre everyone.md? Bueno, aquí tenemos tres estados distintos de este archivo: su estado en la base de fusión, su estado en la rama de John, y su estado en la rama de Paul. Mientras se realiza una fusión, Git almacena todas estas versiones en el índice.

Observemos eso mirando directamente al índice con el comando git ls-files:

git ls-files -s --abbrev
The output of  after the merge operation
The output of git ls-files -s --abbrev after the merge operation

Puedes ver que everyone.md tiene tres entradas diferentes. Git le asigna a cada versión un número que representa la "fusión" del archivo, y esta es una propiedad distinta de una entrada de índice, junto con el nombre del archivo y el modo bits.

Cuando no hay conflicto de fusión en cuanto a un archivo, su "área de preparación" es 0. Esto es de hecho el estado para across_the_universe.md, y para let_it_be.md.

En un estado de conflicto, tenemos:

  • Estado 1 - el cual es la base de fusión.
  • Estado 2 - el cual es "tu" versión. Eso es, la versión del archivo en la rama en la que estás fusionando. En nuestro ejemplo, este sería john_branch_4.
  • Estado 3- el cual es "su" versión, también llamado el MERGE_HEAD. Eso es, la versión en la rama que estás fusionando (en la rama actual). En nuestro ejemplo, ese es paul_branch_4.

Para observar los contenidos del archivo en un estado específico, puedes usar un comando que introduje en un post previo, git cat-file, y proveer el SHA del blob:

git cat-file -p <BLOB_SHA_FOR_STAGE_2>
Using -file to present the content of the file on John's branch, right from its state in the index
Using git cat-file to present the content of the file on John's branch, right from its state in the index

Y justamente, este es el contenido que esperábamos - de la rama de John, donde las líneas comienzan con "Everybody" en vez de "Everyone".

Un lindo truco que te permite ver el contenido rápidamente sin proveer el valor SHA-1 del blob, es usando git show, así:

git show :<STAGE>:everyone.md

Por ejemplo, para obtener el contenido de la misma versión como con git cat-file -p <BLOB_SHA_FOR_STAGE_2>, puedes escribir git show :2:everyone.md.

Git registra los tres estados de los tres confirmaciones en el índice de esta forma al comienzo de la fusión. Luego continúa con el algoritmo de fusión de tres vías para resolver rápidamente los casos sencillos.

En el caso que todas las tres áreas de preparación coincidan, entonces la selección es trivial.

Si un lado hiciera un cambio mientras los otros no lo hicieron - eso es, área de preparación 1 coincide con el área de preparación 2 - entonces elegimos el área de preparación 3, o vice versa. Eso es exactamente lo que sucedió con let_it_be.md y across_the_universe.md.

En caso de una supresión en la rama entrante, por ejemplo, y dado que no hubieron cambios en la rama actual, entonces veríamos que el stage 1 coincide con el stage 2, pero no hay stage 3. En este caso, git merge quita el archivo para la versión fusionada.

Lo que es realmente interesante aquí es para coincidir, Git no necesita los archivos actuales. Más bien, se puede basar en los valores SHA-1 de los blobs correspondientes. De esta forma, Git puede detectar fácilmente el estado en el que se encuentra un archivo.

Git performs the same 3-way merge algorithm on a files level
Git performs the same 3-way merge algorithm on a files level

Para everyone.md tiene este caso especial - donde el área de preparación 1, el área de preparación 2 y el área de preparación 3 son todos distintos uno del otro. Eso es, tienen SHAs de blob distintos. Es momento de ir más profundo y entender el conflicto de fusión. 😊

Una forma de hacer eso sería simplemente usar git diff. En un capítulo anterior, examinamos git diff en detalle, y vimos que muestra las diferencias entre varias combinaciones del árbol de trabajo, del índice o de las confirmaciones.

Pero git diff también tiene un modo especial para ayudar con los conflictos de fusión:

git diff
The output of  during a merge conflict
The output of git diff during a merge conflict

Esta salida podría ser confuso al principio, pero una vez que te acostumbras, es bastante claro. Empecemos por entenderlo, y luego veamos cómo puede resolver los conflictos con otras herramientas mas visuales.

La sección en conflictos está separado por los signos "iguales"  (===), y marcado con las ramas correspondientes. En este contexto, "el nuestro" es la rama actual. En este ejemplo, sería john_branch_4, la rama a la que HEAD estaba apuntando cuando iniciamos el comando git merge. "El suyo" es el MERGE_HEAD, la rama que estamos fusionando - en este caso, paul_branch_4.

Así que git diff sin ningun argumento especial muestra los cambios entre el árbol de trabajo y el índice - el cual en este caso son los conflictos aún por resolver. La salida no incluye cambios en el área de preparación, lo cual es conveniente para resolver el conflicto.

Es tiempo de resolver esto manualmente. ¡Qué divertido!

Así, ¿por qué esto es un conflicto?

Para Git, Paul y John hicieron distintos cambios a la misma línea, por unas pocas líneas. John lo cambió a una cosa, y Paul lo cambió a otra cosa. Git no puede decidir cuál es el correcto.

Este es no es el caso para las últimas líneas, como la línea solía ser "Everyone had a hard year" en la base de fusión. Paul no ha cambiado esta línea, o las líneas que le rodean, así su versión en paul_branch_4 o "el suyo" en nuestro caso, se pone de acuerdo con el merge_base. Aunque la versión de John, "el nuestro", es distinto. Así git merge puede decidir fácilmente que tome esta versión.

Pero, ¿qué hay sobre las líneas en conflicto?

En este caso, sé lo que quiero, y eso es en realidad una combinación de estas líneas. Quiero que las líneas comiencen con "Everybody", siguiendo el cambio de John, pero también que incluya los "yeah" de Paul. Así que ve y crea la versión deseada editando a everyone.md:

nano everyone.md
Editing the file manually to achieve the desired state
Editing the file manually to achieve the desired state

Para comparar el archivo resultante con lo que tenías en la rama previa a la fusión, puedes ejecutar:

git diff --ours

De manera similar, si deseas ver cuánto difiere el resultado de la fusión de la rama que fusionaste en nuestra rama, puedes ejecutar:

git diff --theirs

Inclusive puedes ver cuán distinto es el resultado de ambos lados usando:

git diff --base

Ahora puedes poner en el área de preparación la versión arreglada:

git add everyone.md

Después de poner en el área de preparación, si miras en git status, no verás conflictos:

After staging the fixed version , there are no conflicts
After staging the fixed version everyone.md, there are no conflicts

Ahora puedes simplemente usar git commit, y Git te presentará con un mensaje de confirmación conteniendo los detalles sobre la fusión. Puedes modificarlo si quisieras, o dejarlo como está. Sin importar del mensaje de confirmación, Git creará un "mensaje de fusión" - eso es, una confirmación con más de un precesor.

Para validar eso, considera el historial:

The history after completing the merge operation
The history after completing the merge operation

john_branch_4 ahora apunta a la nueva confirmación de fusión. La rama entrante, "el suyo", en este caso, paul_branch_4, se mantiene donde estaba.

Cómo usar VS Code para resolver los conflictos

Ahora verás cómo resolver el mismo conflicto usando una herramienta gráfica. Para este ejemplo, use VS Code, el cual es un editor de código popular y gratis. Hay muchas otras herramientas, pero el proceso es similar, así que solo mostraré VS Code como un ejemplo.

Primero, volvamos al estado antes de la fusión:

git reset --hard HEAD~

E intentemos fusionar otra vez:

git merge paul_branch_4

Deberías estar nuevamente al mismo estado:

Back at the conflicting status
Back at the conflicting status

Veamos cómo aparece esto en VS Code:

Conflict resolution with VS Code
Conflict resolution with VS Code

VS Code marca las distintas versiones con "Current Change" - el cual es la versión de la "nuestra", el actual HEAD, y "Cambio Entrante" para la rama que estamos fusionando en la rama activa. Puedes aceptar uno de los cambios (o ambos) haciendo clic en una de las opciones.

Si hiciste clic en Resolver en Editor de Fusión, obtendrás una vista mas visual del estado. VS Code muestra el estado de cada línea:

VS Code's Merge Editor
VS Code's Merge Editor

Si miras con atención, verás que VS Code muestra cambios dentro de las palabras - por ejemplo, muestra que "Everyone" fue cambiado a "Everybody", marcando las partes cambiadas.

Puedes aceptar cualquier versión, o puedes aceptar una combinación. En este caso, si haces clic en "Aceptar Combinación", obtienes este resultado:

VS Code's Merge Editor after clicking on "Accept Combination"
VS Code's Merge Editor after clicking on "Accept Combination"

¡VS Code hizo un excelente trabajo! El mismo algoritmo de fusión de tres vías fue implementado aquí y usado a el nivel de palabra en vez a nivel de línea. Así que VS Code fue capaz en realidad de resolver este conflicto de una forma impresionante. Por supuesto, puedes modificar la sugerencia de VS Code, pero proveyó un comienzo muy bueno.

Una Herramienta Poderosa adicional

Bueno, este fue la primera vez en este libro que he usado una herramienta con una interfaz de usuario gráfica. De hecho, las interfaces gráficas pueden ser convenientes para entender lo que está sucediendo cuando estás resolviendo conflictos de fusión.

Sin embargo, como en muchos otros casos, cuando necesitamos realmente entender lo que está sucediendo, la línea de comando se vuelve práctico. Así que, volvamos a la línea de comandos y aprendamos una herramienta que nos puede venir bien en casos mas complicados.

De nuevo, vuelve al estado antes de la fusión:

git reset --hard HEAD~

Y fusiona:

git merge paul_branch_4

Y digamos, no estás seguro exactamente de lo que pasó. ¿Por qué hay un conflicto? Un comando muy útil sería:

git log -p --merge

Como recordatorio, git log muestra el historial de confirmaciones que son alcanzables desde HEAD. Agregando -p le dice a git log que muestre las confirmaciones juntamente con los diffs que introdujeron. El argumento --merge hace que el comando muestre todas las confirmaciones que contienen cambios relevantes a cualquier archivo que no están en el área de preparación, en cualquier rama, junto con sus diffs.

Esto te puede ayudar en identificar los cambios en el historial que llevaron a los conflictos. Así que en este ejemplo, verías:

The output of
The output of git log -p --merge

La primer confirmación que vemos es "Commit 15", ya que en ésta confirmación John modificó everyone.md, un archivo que todavía tiene conflictos. Luego, Git muestra "Commit 13", donde Paul cambió everyone.md:

The output of  - continued
The output of git log -p --merge - continued

Fíjate que git log --merge no mencionó confirmaciones previas que cambiaron a everyone.md antes de "Commit 13", ya que no afectaron el conflicto actual.

De esta forma, git log te dice que todo lo que necesitas saber para entender el proceso que te llevó al estado de conflicto actual. ¡Genial! 😎

Usando la línea de comandos, también puedes preguntarle a Git que tome solamente un lado de los cambios - sea "el nuestro" o "el suyo", inclusive para un archivo específico.

También puedes instruir a Git que tome algunas partes de los diffs de un archivo y otro de otro archivo. Proveeré enlances que describen cómo hacer eso en los recursos adicionales de este capítulo en el apéndice.

Para la mayor parte, puedes lograr eso bastante fácil, sea manualmente o desde la UI de tu IDE favorito.

Por ahora, es tiempo de recapitular.

Recapitulando - Entendiendo Git Merge

En este capítulo, tuviste una vista general extensiva de fusión con Git. Aprendiste que fusionar es el proceso de combinar los cambios recientes desde varias ramas en una sola confirmación. La nueva confirmación tiene dos predecesores - esas confirmaciones los cuales han sido las puntas de las ramas que fueron fusionados.

Consideramos una fusión directa sencilla, lo cual es posible cuando una rama difiere de la rama base, y luego agregamos confirmaciones por encima de la rama base.

Luego consideramos fusiones de tres vías, y explicamos el proceso de tres estados:

  • Primero, Git localiza la base de fusión. Como recordatorio, este es la primer confirmación que es alcanzable desde ambas ramas.
  • Segudno, Git calcula los dos diffs - un diff desde la base de fusión a la primer rama, y otro diff desde la base de fusión a la segunda rama. Git genera parches basados en esos diffs.
  • Tercero y último, Git aplica ambos parches a la base de fusión usando un algoritmo de fusión de 3 vías. El resultado es el estado de la nueva confirmación de fusión.

Indagamos profundamente en el proceso de fusión de 3 vías, sea a nivel de archivo o a nivel de hunk. Consideramos cuando Git es capaz de basarse en una fusión de 3 vías para resolver automáticamente los conflictos, y cuando no puede.

Viste la salida de git diff cuando estamos en un estado de conflicto, y cómo resolver los conficltos manualmente o con VS Code.

Hay mucho más que decir sobre las fusiones - diferentes estrategias de fusión, fusiones recursivas, y así sucesivamente. Con todo, creo que este capítulo cubrió todo lo necesario de esa forma tienes un entendimiento robusto de lo que es una fusión, y qué sucede por debajo en la vasta mayoría de casos.

Recursos relacionados a los Beatles

Capítulo 8 - Entendiendo Git Rebase

Una de las herramientas más poderosas que tiene un desarrollador en su caja de herramientas es git rebase. Aunque es notorio por ser complejo e incomprendido.

La verdad es, si entiendes lo que hace en realidad, git rebase es una herramienta muy elegante, y directa para alcanzar muchísimas cosas diferentes en Git.

En los capítulos previos en esta parte, aprendiste qué son los diffs de Git, lo que es una fusión, y cómo Git resuelve los conflictos de fusión. En este capítulo, entenderás lo que es rebase de Git, por qué es diferente de merge, y cómo hacer rebase (sobrescribir la base) con confianza.

Recapitulación Corta - ¿Qué es Git Merge?

Por debajo, git rebase y git merge son cosas muy muy distintas. Entonces, ¿por qué la gente los compara todo el tiempo?

La razón es su uso. Cuando se trabaja con Git, usualmente trabajamos en distintas ramas e introducimos cambios a esas ramas.

En el capítulo previo, consideramos el ejemplo donde John y Paul (de los Beatles) eran co-autores de nueva canción. Comenzaron desde la rama main, y luego cada uno difirió, modificaron la letra, y confirmaron sus cambios.

Luego, los dos querían integrar sus cambios, lo cual es algo que sucede muy frecuente cuando se trabaja con Git.

A diverging history -  and  diverged from
A diverging history - paul_branch and john_branch diverged from main

Hay dos formas principales para integrar cambios introducidos en diferentes ramas en Git, o en otras palabras, en diferentes confirmaciones e historiales de confirmación. Estos son merge y rebase.

En el capítulo anterior, llegamos a saber git merge bastante bien. Vimos que cuando realizamos una fusión, creamos una confirmación de fusión - donde los contenidos de esta confirmación son una combinación de las dos ramas, y también tiene dos predecesores, una en cada rama.

Así que, digamos que estás en la rama john_branch (asumiendo el historial representado en el dibujo de arriba), y ejecutas git merge paul_branch. Llegarás a este estado - donde en john_branch, hay una nueva confirmación con dos predecesores. El primero será la confirmación en la rama john_branch donde el HEAD estaba apuntando a un estado antes de realizar la fusión - en este caso, "Commit 6". El segundo será la confirmación a la que apunta paul_branch, "Commit 9".

The result of running : a new Merge Commit with two parents
The result of running git merge paul_branch: a new Merge Commit with two parents

Mira otra vez al gráfico del historial: creaste un historial divergente. Puedes ver dónde se ramificó y dónde se fusionó nuevamente.

Así que cuando usas git merge, no puedes reescribir el historial - sino, que agregas una confirmación al historial existente. Y específicamente, una confirmación que crea un historial divergente.

¿Cómo se diferencia git rebase de git merge?

Cuando se usa git rebase, algo distinto ocurre.

Comencemos con el panorama general: si estás en paul_branch, y usas git rebase john_branch, Git va al ancestro común de la rama de John y de Paul. Luego tomas los parches introducidos en las confirmaciones de la rama de Paul, y aplicar esos cambios a la rama de John.

Así que aquí, usas rebase para tomar los cambios que fueron confirmados en una rama - la rama de Paul - y reproducirlos en una rama distinta, john_branch.

The result of running : the commits on  were "replayed" on top of
The result of running `git rebase john_branch`: the commits on `paul_branch` were "replayed" on top of john_branch

Espera, ¿qué significa eso?

Ahora tomaremos esto poco a poco para asegurarnos que entiendes completamente lo que estás sucediendo por debajo 😎.

cherry-pick como una base para Rebase

Es útil pensar de rebase como realizar git cherry-pick - un comando que toma una confirmación, calcula el parche que ésta confirmación introduce al calcular la diferencia entre la confirmación del predecesor y la confirmación misma, y luego cherry-pick "repite" esta diferencia.

Hagamos esto manualmente.

Si miramos la diferencia introducido por "Commit 5" al hacer git diff main <SHA_OF_COMMIT-5>:

Running  to observe the patch introduced by "Commit 5"
Running git diff to observe the patch introduced by "Commit 5"

Como siempre, se te anima en ejecutar los comandos tú mismo mientras lees este capítulo. A menos que se diga otra cosa, usaré el siguiente repositorio:

https://github.com/Omerr/rebase_playground.git

Te recomiendo que lo clones de forma local y tener el mismo punto de inicio que estoy usando para este capítulo.

Puedes ver eso en esta confirmación, John comenzó trabajando en una canción llamada "Lucy in the Sky with Diamonds":

The output of  - the patch introduced by "Commit 5"
The output of git diff - the patch introduced by "Commit 5"

Como recordatorio, también puedes usar el comando git show para obtener la misma salida:

git show <SHA_OF_COMMIT_5>

Ahora, si haces cherry-pick a esta confirmación, introducirás este cambio específicamente, en la rama activa. Cambia a main primero:

git checkout main (or git switch main)

Y crea otra rama:

git checkout -b my_branch (or git switch -c my_branch)
Creating  that branches from
Creating my_branch that branches from main

Luego, haz cherry-pick a "Commit 5":

git cherry-pick <SHA_OF_COMMIT_5>
Using  to apply the changes introduced in "Commit 5" onto
Using cherry-pick to apply the changes introduced in "Commit 5" onto main

Considera el log (salida de git lol):

The output of
The output of git lol

Parece que copiaste-pegaste el "Commit 5". Recuerda que inclusive tiene el mismo mensaje de confirmación, e introduce los mismos cambios, e inclusive apunta al mismo objeto de árbol como el "Commit 5" original en este caso - todavía es un objeto de confirmación distinto, como si fuese creado con una marca de tiempo distinto.

Mirando a los cambios, usando git show HEAD

The output of
The output of git show HEAD

Son lo mismo como los "Commit 5".

Y por supuesto, si miras al archivo (digamos, usando nano lucy_in_the_sky_with_diamonds.md), estará en el mismo estado que estaba después del "Commit 5" original.

¡Genial! 😎

Ahora puedes quitar la nueva rama así no aparece en tu historial cada vez:

git checkout main
git branch -D my_branch

Más allá de cherry-pick - Cómo usar git rebase

Puedes ver a git rebase como una forma de realizar múltiples cherry-pick uno después del otro - eso es, "repetir" múltiples confirmaciones. Esto no es lo único que puedes hacer con rebase, pero es un buen punto de comienzo para nuestra explicación.

¡Es tiempo de jugar con git rebase!

Antes, hiciste fusión de paul_branch en john_branch. ¿Qué debería de pasar si hicieras rebase de paul_branch por encima de john_branch? Obtendrías un historial muy distinto.

En esencia, parecería como si tomáramos los cambios introducidos en las confirmaciones de paul_branch, y repitiéramos en john_branch. El resultado sería un historial linear.

Para entender el proceso, te proveeré la vista de alto nivel, y luego indagaré más profundo en cada paso. El proceso de hacer rebase en una rama por encima de otra rama es como lo siguiente:

  1. Encuentra el ancestro común.
  2. Identifica las confirmaciones a ser "repetidos".
  3. Para cada confirmación X, calcula diff(parent(X), X), y lo almacena como un patch(X).
  4. Mueve a HEAD a la nueva base.
  5. Aplica los parches generados en orden en la rama de destino. Cada vez, crea un nuevo objeto de confirmación con el nuevo estado.

El proceso de hacer nuevas confirmaciones con el mismo conjunto de cambios que los existentes también se le llama "repetir" esas confirmaciones, un término que ya hemos usado.

Tiempo de practicar con Rebase

Antes de ejecutar el siguiente comando, asegúrate de tener a john_branch localmente, así que ejecuta:

git checkout john_branch

Comienza desde la rama de Paul:

git checkout paul_branch

Este es el historial:

Commit history before performing
Commit history before performing git rebase

Y ahora, la parte emocionante:

git rebase john_branch

Y observa el historial:

The history after rebasing
The history after rebasing

Con git merge agregaste al historial, mientras que con git rebase reescribes el historial. Creas nuevos objetos de confirmación. Además, el resultado es un gráfico de historial linear - en vez de un gráfico divergente.

The history after rebasing
The history after rebasing

En esencia, "copiaste" las confirmaciones que estaban en paul_branch y que fueron introducidos después de "Commit 4", y los "pegaste" por encima de john_branch.

El comando se llama "rebase", porque cambia la confirmación base de la rama de donde se ejecuta. Eso es, en tu caso, antes de ejecutar git rebase, la base de paul_branch era "Commit 4" - ya que aquí es donde la rama "nació" (desde main). Con rebase, le pediste a Git que le diera otra base - eso es, pretender que haya nacido desde "Commit 6".

Para hacer eso, Git tomó lo que solía ser "Commit 7", y "repitió" los cambios introducidos en esta confirmación en "Commit 6". Luego creó un nuevo objeto de confirmación. Este objeto difiere del "Commit 7"  original en tres aspectos:

  1. Tiene una marca de tiempo distinto.
  2. Tiene una confirmación antecesora distinta - "Commit 6", en vez de "Commit 4".
  3. El objeto árbol al que está apuntado es distinto - como si los cambios fueran introducidos en el árbol apuntado por "Commit 6", y no el árbol apuntado por "Commit 4".

Fíjate la última confirmación aquí, "Commit 9".  La copia instantánea que representa (eso es, el árbol al que apunta) es exactamente el mismo árbol que obtendrías al hacer fusión de las dos ramas. El estado de los archivos en tu repositorio Git sería el mismo como si usaras git merge. Es solamente el historial que es diferente, y los objetos de confirmación por supuesto.

Ahora, puedes usar simplemente:

git checkout main
git merge paul_branch

Hm..... ¿qué sucedería si ejecutas este último comando? Considera el historial de confirmación de nuevo, después de verificar a main:

The history after rebasing and checking out
The history after rebasing and checking out main

¿Qué significaría el hacer fusión de main y paul_branch?

En efecto, Git puede simplemente hacer una fusión directa, ya que el historial es completamente linear (si necesitas un recordatorio sobre fusiones directos, mira el capítulo previo). Como resultado, main y paul_branch ahora apunta a la misma confirmación:

The result of a fast-forward merge
The result of a fast-forward merge

Rebase avanzado en Git

Ahora que entiendes lo básico de rebase, es tiempo de considerar casos más avanzados, donde conmutadores y argumentos adicionales al comando rebase serán útiles.

En el ejemplo previo, cuando solamente usaste rebase (sin conmutadores adicionales), Git repitió todas las confirmaciones desde el ancestro común a la punta de la rama actual.

Pero rebase es un super poder. Es un comando todopoderoso capaz de.. bueno, reescribir el historial. Y puede ser útil si quieres modificar el historial para hacer el tuyo propio.

Deshaz la última fusión haciendo que main apunte a "Commit 4" nuevamente:

git reset --hard <ORIGINAL_COMMIT 4>
"Undoing" the last merge operation
"Undoing" the last merge operation

Y deshaz el rebasing usando:

git checkout paul_branch
git reset --hard <ORIGINAL_COMMIT 9>
"Undoing" the rebase operation
"Undoing" the rebase operation

Fíjate que obtuviste exactamente el mismo historial que solías tener:

Visualizing the history after "undoing" the rebase operation
Visualizing the history after "undoing" the rebase operation

Para ser claro, "Commit 9" no sólo desaparece cuando no es alcanzable desde el HEAD actual. Al contrario, todavía es almacenado en la base de datos de objetos. Y como usaste git reset ahora para hacer que HEAD apunte a esta confirmación, fuiste capaz de recuperarlo, y también sus confirmaciones antecesoras ya que también están almacenados en la base de datos. Bastante genial, ¿hah? 😎

Aprenderás más sobre git reset en la próxima parte, donde discutimos sobre deshacer cambios en Git.

Mira los cambios que Paul introdujo:

git show HEAD
 shows the patch introduced by "Commit 9"
git show HEAD shows the patch introduced by "Commit 9"

Sigue yendo hacia atrás en el gráfico de confirmación:

git show HEAD~
 (same as ) shows the patch introduced by "Commit 8"
git show HEAD~ (same as git show HEAD~1) shows the patch introduced by "Commit 8"

Y una confirmación más allá:

git show HEAD~2
 shows the patch introduced by "Commit 7"
git show HEAD~2 shows the patch introduced by "Commit 7"

Tal vez Paul no quiere este tipo de historial. Más bien, quiere que parezca como si se introdujera los cambios en "Commit 7" y "Commit 8" como una sola confirmación.

Para eso, puedes usar un rebase interactivo. Para hacer eso, agregamos el conmutador -i (o --interactive) al comando rebase:

git rebase -i <SHA_OF_COMMIT_4>

O, ya que main está apuntando a "Commit 4" , podemos ejecutar:

git rebase -i main

Ejecutando este comando, le dices a Git que use una nueva base, "Commit 4". Así que le pides a Git que vuelva atrás a todas las confirmaciones que fueron introducidos después de "Commit 4" y que son alcanzables  desde el HEAD actual, y repita esas confirmaciones.

Para cada confirmación que es repetido, Git nos pregunta que nos gustaría hacer con él:

 prompts you to select what to do with each commit
git rebase -i main prompts you to select what to do with each commit

En este contexto es útil imaginarse de una confirmación como un parche. Eso es, "Commit 7", como "el parche que "Commit 7" introdujo por encima de su antecesor".

Una opción es usar pick. Este es el comportamiento predeterminado, el cual le dice a Git que repita los cambios introducidos en esta confirmación. En este caso, si sólo lo dejas como está - y haces pick de todas las confirmaciones - obtendrás el mismo historial, y Git no creará nuevos objetos de confirmación.

Otra opción es squash. Una confirmación aplastada (del inglés "squashed") tendrá su contenido "doblado" (del inglés "folded") en los contenidos de la confirmación que le precede. Así que en nuestro caso, a Paul le gustaría aplastar "Commit 8" en "Commit 7":

Squashing "Commit 8" into "Commit 7"
Squashing "Commit 8" into "Commit 7"

Como puedes ver, git rebase -i provee opciones adicionales, pero no lo veremos en este capítulo. Si permites a rebase que se ejecute, se te mostrará una ventana para seleccionar un mensaje de confirmación para la nueva confirmación creada (eso es, el que introdujo los cambios de "Commit 7" y "Commit 8"):

Providing the commit message: Commits 7+8
Providing the commit message: Commits 7+8

Y mira el historial:

The history after the interactive rebase
The history after the interactive rebase

¡Exactamente lo que queríamos! En paul_branch, tenemos "Commit 9" (por supuesto, es un objeto distinto que el "Commit 9" original). Este objeto apunta a "Commit 7+8", el cual es una confirmación única que introduce los cambios del "Commit 7" original y el "Commit 8" original. El antecesor de la confirmación es "Commit 4", a donde main está apuntando.

Oh wow, ¿no es genial? 😎

git rebase te concede control sin límites sobre la figura de cualquier rama. Puedes usarlo para re-ordenar confirmaciones, o para quitar cambios incorrectos, o modificar un cambio en retrospectiva. Alternativamente, tal vez podrías mover la base de tu rama en otra confirmación, cualquier confirmación que desees.

Cómo usar el conmutador --onto de git rebase

Consideremos un ejemplo más. Vuelve a main nuevamente:

git checkout main

Y elimina los punteros de paul_branch y john_branch así ya no los ves en el gráfico de la confirmación:

git branch -D paul_branch
git branch -D john_branch

Luego, haz una nueva rama desde main:

git checkout -b new_branch
Creating  that diverges from
Creating new_branch that diverges from main

Este es el historial limpio que deberías tener:

A clean history with  that diverges from
A clean history with new_branch that diverges from main

Ahora, cambia el archivo code.py (por ejemplo, agrega una nueva función) y confirma tus cambios:

nano code.py
Adding the function  to
Adding the function new_branch to code.py
git add code.py
git commit -m "Commit 10"

Vuelve a main:

git checkout main

E introduce otro cambio - agregando una cadena de documentos al principio del archivo:

Added a docstring at the beginning of the file
Added a docstring at the beginning of the file

Es tiempo de poner en el área de preparación y confirmar estos cambios:

git add code.py
git commit -m "Commit 11"

Y aún otro cambio, tal vez agrega @Author a la cadena de documentos:

Added  to the docstring
Added @Author to the docstring

Confirma este cambio también:

git add code.py
git commit -m "Commit 12"

Oh espera, ahora me doy cuenta que quería que tú hicieras los cambios introducidos en "Commit 11" como parte de new_branch. Ugh. ¿Qué puedes hacer?

Considera el historial:

The history after introducing "Commit 12"
The history after introducing "Commit 12"

En vez de tener que "Commit 11" resida solamente en la rama main, quiero que esté en ambas ramas main y new_branch. Visualmente, quisiera moverlo hacia abajo en el gráfico aquí:

Visually, I want you to "push down" "Commit 10"
Visually, I want you to "push down" "Commit 10"

¿Puedes ver a dónde estoy yendo? 😇

Bueno, rebase te permite básicamente repetir los cambios introducidos en new_branch, aquellos introducidos en "Commit 10", como si hayan sido originalmente realizados sobre "Commit 11", en vez de "Commit 4".

Para hacer eso, puedes usar otros argumentos de git rebase. Específicamente, puedes usar git rebase --onto, el cual opcionalmente toma tres parámetros:

git rebase --onto <new_parent> <old_parent> <until>

Eso es, tomas todas las confirmaciones entre old_parent y until, y los "cortas" y "pegas" en new_parent.

En este caso, le dirías a Git que quieres tomar todo el historial introducidos entre el ancestro común de main y new_branch, el cual es "Commit 4", y que sea la nueva base para ese historial "Commit 11". Para hacer eso, usa:

git rebase --onto <SHA_OF_COMMIT_11> main new_branch
The history before and after the rebase, "Commit 10" has been "pushed"
The history before and after the rebase, "Commit 10" has been "pushed"

¡Y mira nuestro hermoso historial! 😍

The history before and after the rebase, "Commit 10" has been "pushed"
The history before and after the rebase, "Commit 10" has been "pushed"

Consideremos otro caso.

Digamos que empecé a trabajar en una nueva característica, y por error comencé a trabajar desde feature_branch_1, en lugar de main.

Así que para emular esto, crea feature_branch_1:

git checkout main
git checkout -b feature_branch_1

Y elimina new_branch así ya no lo ves en el gráfico:

git branch -D new_branch

Crea un archivo Python sencillo llamado 1.py:

A new file, , with
A new file, 1.py, with print('Hello world!')

Pónlo en el área de preparación y confirma este archivo:

git add 1.py
git commit -m  "Commit 13"

Ahora haz una nueva rama desde feature_branch_1 (este es el error arreglará luego):

git checkout -b feature_branch_2

Y crea otro archivo, 2.py:

Creating
Creating 2.py

Pónlo en el área de preparación y confirma este archivo también:

git add 2.py
git commit -m  "Commit 14"

E introduce algo de código a 2.py:

Modifying
Modifying 2.py

Pónlo en el área de preparación y confirma estos cambios también:

git add 2.py
git commit -m  "Commit 15"

Hasta ahora tendrías este historial:

The history after introducing "Commit 15"
The history after introducing "Commit 15"

Vuelve a feature_branch_1 y edita 1.py:

git checkout feature_branch_1
Modifying
Modifying 1.py

Ahora pónlo en el área de preparación y confirma:

git add 1.py
git commit -m  "Commit 16"

Tu historial debería lucir así:

The history after introducing "Commit 16"
The history after introducing "Commit 16"

Digamos que ahora te das cuenta que cometiste un error. En realidad querías que feature_branch_2 naciera de la rama main, en vez de feature_branch_1.

¿Cómo puedes lograr eso?

Trata de imaginarlo dado el gráfico del historial y lo que has aprendido sobre el argumento --onto para el comando rebase.

Bueno, quieres "reemplazar" el antecesor de tu primera confirmación en feature_branch_2, el cual es "Commit 14", que está por encima de la rama main - en este caso, "Commit 12" - en vez del comienzo de feature_branch_1 - en este caso, "Commit 13". Así que devuelta, estarás creando una nueva base, esta vez para la primera confirmación en feature_branch_2.

You want to move around "Commit 14" and "Commit 15"
You want to move around "Commit 14" and "Commit 15"

¿Cómo harías eso?

Primero, cambia a feature_branch_2:

git checkout feature_branch_2

Y ahora puedes usar:

git rebase --onto main <SHA_OF_COMMIT_13>

Esto le dice a Git que tome el historial con "Commit 13" como una base, y cambia esa base para que sea "Commit 12" (apuntado por main).

Como resultado, tienes a feature_branch_2 que tiene su base en main en vez de feature_branch_1:

The commit history after performing rebase
The commit history after performing rebase

La sintaxis del comando es:

git rebase --onto <new_parent> <old_parent>

Cómo hacer rebase en una sola rama

También puedes usar git rebase mientras miras el historial de una sola rama.

Veamos si puedes ayudarme aquí.

Digamos que trabajé en feature_branch_2, específicamente edité el archivo code.py. Comencé cambiando todas las cadenas para que sean envueltos por doble comillas en vez de comillas simples.

Changing  into  in
Changing ' into " in code.py

Luego, los puse en el área de preparación y los confirmé:

git add code.py
git commit -m "Commit 17"

Luego decidí agregar una nueva función al principio del archivo:

Adding the function
Adding the function another_feature

Nuevamente, los puse en el área de preparación y confirmé:

git add code.py
git commit -m "Commit 18"

Y ahora me doy cuenta que en realidad me olvidé de cambiar las comillas simples a comillas dobles envolviendo a __main__ (como has notado), así que hice eso también:

Changing  into
Changing '__main__' into "__main__"

Por supuesto. puse este cambio en el área de preparación y confirmé:

git add code.py
git commit -m "Commit 19"

Ahora, considera el historial:

The commit history after introducing "Commit 19"
The commit history after introducing "Commit 19"

¿No es genial, no? O sea, tengo dos confirmaciones que están relacionados el uno al otro, "Commit 17" y "Commit 19" (cambiando los ' a "), pero están separados por "Commit 18" que no está relacionado (donde agregué una nueva función). ¿Qué podemos hacer? ¿Puedes ayudarme?

Intuitivamente, quiero editar el historial aquí:

These are the commits I want to edit
These are the commits I want to edit

Así que, ¿qué harías?

¡Exacto!

Puedo hacer rebase del historial de "Commit 17" a "Commit 19", por encima de "Commit 15". Para hacer eso:

git rebase --interactive --onto <SHA_OF_COMMIT_15> <SHA_OF_COMMIT_15>

Fíjate que especifiqué "Commit 15" como el comienzo del rango de confirmaciones, excluyendo esta confirmación. Y no necesité especificar explícitamente a HEAD como el último parámetro.

Using  on a single branch
Using rebase --onto on a single branch

(Nota: si sigues los pasos de arriba con mi repositorio y obtienes un conflicto de fusión, podrías tener una configuración distinta al de mi máquina debido a los caracteres de espacio en blanco en las líneas finales. En ese caso, puedes agregar el argumento --ignore-whitespace) al comando rebase, resultando en el siguiente comando: git rebase --ignore-whitspace --interactive --onto <SHA_OF_COMMIT_15> <SHA_OF_COMMIT_15>. Si quisieras saber más sobre este problema, busca autocrlf.)

Después de seguir tu consejo y ejecutar el comando rebase (¡gracias! 😇) obtengo la siguiente pantalla:

Interactive rebase
Interactive rebase

Así que, ¿qué haría? Quiero poner a "Commit 19" antes de "Commit 18", de esa forma viene después de "Commit 17". Puedo ir más lejos y hacer squash (aplastarlos juntos), así:

Interactive rebase - changing the order of commit and squashing
Interactive rebase - changing the order of commit and squashing

Ahora cuando se me aparece una ventana para un mensaje de confirmación, puedo proveer el mensaje "Commit 17+19":

Providing a commit message
Providing a commit message

Y ahora, mira nuestro hermoso historial:

The resulting history
The resulting history

¡Gracias nuevamente!

Más casos de uso de Rebase + Más práctica

Por ahora espero que te sientas cómodo con la sintaxis de rebase. La mejora forma de entenderlo en sí es considerar varios casos y averiguar cómo resolverlos tú mismo.

Con los casos que vienen, te recomiendo enérgicamente que dejes de leer después de lo que introduje en cada caso de uso, y luego intentes resolverlo por ti mismo.

Cómo excluir confirmaciones

Digamos que tienes este historial en otro repo:

Another commit history
Another commit history

Antes de jugar con él, almacena una etiqueta a "Commit F" así puedes recuperarlo más tarde:

git tag original_commit_f

(Una etiqueta es una referencia nombrada a una confirmación, como una rama - pero no cambia cuando agregas confirmaciones adicionales. Es como una referencia nombrada constante.)

Ahora, en realidad no quieres los cambios en "Commit C" y "Commit D" que sean incluidos. Podrías usar un rebase interactivo como antes y quitar sus cambios. O, podrías usar git rebase --onto de nuevo. ¿Cómo usarías --onto para "quitar" estas dos confirmaciones?

Puedes hacer rebase de HEAD por encima de "Commit B", donde el antecesor viejo era en realidad "Commit D", y ahora debería ser "Commit B". Considera el historial nuevamente:

The history again
The history again

Hacer rebase para que "Commit B" sea la base de "Commit E" significa "mover" ambos "Commit E" y "Commit F", y darles otra base - "Commit B". ¿Puedes idearte el comando tú mismo?

git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D> HEAD

Fíjate que usar la sintaxis de arriba (exactamente como se proveyó) no haría que main apunte a la nueva confirmación, para que el resultado sea un HEAD "separado". Si usas gg u otra herramienta que muestra el historial alcanzable desde las ramas, te podría confundir:

Rebasing with  results in a detached
Rebasing with --onto results in a detached HEAD

Pero si simplemente usas git log (o mi alias git lol), verás el historial deseado:

The resulting history
The resulting history

No sé tú, pero este tipo de cosas me hacen realmente feliz. 😊😇

Por cierto, podría omitir el HEAD del comando previo como si este fuera el valor predeterminado para el tercer parámetro. Así que usar:

git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D>

Tendría el mismo efecto. El último parámetro en realidad le dice a Git donde está el final de la secuencia actual de las confirmaciones para hacerles rebase. Así que la sintaxis de git rebase --onto con los tres argumentos es:

git rebase --onto <new_parent> <old_parent> <until>

Cómo mover confirmaciones a través de las ramas

Así que digamos que obtenemos el mismo historial que antes:

git checkout original_commit_f

Y ahora quiero solamente que "Commit E" esté en una rama que tenga su base en "Commit B". Eso es, quiero tener una nueva rama, que venga desde "Commit B", que tenga solamente a "Commit E".

The current history, considering "Commit E"
The current history, considering "Commit E"

Así que, ¿qué significa esto en términos de rebase? Considera la imagen de arriba. ¿Qué confirmación (o confirmaciones) debería hacer rebase, y qué confirmación debería ser la nueva base?

Sé que puedo contar contigo aquí 😉

Lo que quiero es tomar el "Commit E", y solamente esta confirmación, y cambiar su base para que sea "Commit B". En otras palabras, repetir los cambios introducidos en "Commit E" en "Commit B".

¿Puedes aplicar esa lógica a la sintaxis de git rebase?

Aquí está (esta vez estoy escribiendo <COMMIT_X> en vez de <SHA_OF_COMMIT_X>, para brevedad):

git rebase --onto <COMMIT_B> <COMMIT_D> <COMMIT_E>

Ahora el historial luce así:

The history after rebase
The history after rebase

Fíjate que rebase movió a HEAD, pero ninguna otra referencia nombrada (tales como una rama o una etiqueta). En otras palabras, estás en un estado de HEAD separado. Así que aquí también, usando gg u otra herramienta que muestre el historial alcanzable desde las ramas y de las etiquetas te podría confundir. Puedes usar git log (o mi alias git lol) para mostrar el historial alcanzable desde HEAD.

¡Genial!

Una Nota sobre los Conflictos

Fíjate que cuando haces un rebase, podrías entrar en conflictos así como al fusionar. Podrías tener conflictos porque, cuando haces rebase, estás intentando aplicar parches en una base distinta, tal vez donde los parches no apliquen.

Por ejemplo, considera el repositorio nuevamente, y específicamente, considera el cambio introducido en "Commit 12", que es apuntado por main:

git show main
The patch introduced in "Commit 12"
The patch introduced in "Commit 12"

Ya cubrí el formato de git diff en detalle en el capítulo 6, pero como un recordatorio rápido, esta confirmación le instruye a Git que agregue una línea después de las dos líneas de contexto:

```
This is a sample file

Y antes de estas tres líneas de contexto:

```
def new_feature():
  print('new feature')

Digamos que estás intentando hacer rebase de "Commit 12" en otra confirmación. Si, por alguna razón, estas líneas de contexto no existen como sí existen en el parche de la confirmación en la que estás haciendo rebase, entonces tendrías un conflicto.

Aléjandose para ver el panorama general

Comparing rebase and merge
Comparing rebase and merge

Al principio de este capítulo, comencé mencionando la similaridad entre git merge y git rebase: ambos son usados para integrar cambios introducidos en historiales distintos.

Pero, como ahora sabes, son muy distintos en cómo operan. Mientras se fusionan resultados en un historial divergente, hacer rebase resulta en un historial linear. Los conflictos son posibles en ambos casos. Y hay una columna más descrita en la tabla de arriba que requiere más atención.

Ahora que sabes qué es "Git rebase", y cómo usar el rebase interactivo o rebase --onto, como espero que estés de acuerdo, git rebase es una herramienta super poderosa. Aunque, tiene una desventaja grande cuando se compara con fusionar.

Git rebase cambia el historial.

Esto significa que no deberías hacer rebase de las confirmaciones que existen fuera de tu copia local del repositorio, y que otras personas podrían tener su base en sus confirmaciones.

En otras palabras, si sólo las confirmaciones en cuestión son aquellos que creaste de manera local - adelante, use rebase, enloquécete.

Pero si las confirmaciones han sido empujados (enviados), eso puede llevarte a un gran problema - ya que alguien más podría basarse en estas confirmaciones que tu luego sobreescribas, y después tú y ellos tendrás distintas versiones del repositorio.

Esto es distinto a merge el cual, como hemos visto, no modifica el historial.

Por ejemplo, considera el último caso donde cambiamos la base y resultó en este historial:

The history after rebase
The history after rebase

Ahora, digamos que ya empujé esta rama al remoto. Y después que he empujado la rama, otro desarrollador lo empujó e hizo una rama desde "Commit C". El otro desarrollador no sabía que mientras tanto, estuve haciendo rebase de mi rama de forma local, y luego lo subiría (empujaría) nuevamente.

Esto resulta en una inconsistencia: el otro desarrollador trabaja desde una confirmación que ya no está disponible en mi copia del repositorio.

No elaboraré exactamente lo que esto causa en este libro, ya que mi mensaje principal es que deberías evitar definitivamente tales casos. Si estás interesado en qué sucedería en sí, te dejaré un enlace a un recurso útil en las referencias adicionales. Por ahora, resumamos lo que hemos cubierto.

Recapitulación - Entendiendo Git rebase

En este capítulo, aprendiste sobre git rebase, una herramienta super poderosa para reescribir el histoiral en Git. Consideraste unos pocos casos donde git rebase puede ser útil, y cómo usarlo en uno, dos, o tres parámetros, con y sin el conmutador --onto.

Espero que haya sido capaz de convencerte que git rebase es poderoso - pero también que es bastante sencillo una vez que entiendes la esencia. Es una herramienta que puedes usar para "copiar-pegar" confirmaciones (o, más acertadamente, parches). Y es una herramienta útil para tener en tu cinto. En esencia, git rebase toma los parches introducidos por las confirmaciones, y repetirlos en un otra confirmación. Como se describió en este capítulo, esto es útil en muchos escenarios distintos.

Parte 2 - Resumen

En esta parte aprendiste a hacer ramas e integrar cambios en Git.

Aprendiste lo que es un diff, y la diferencia entre un diff y un parche. También aprendiste cómo se construye la salida de git diff.

Entender los diffs es un hito mayor para entender muchos otros procesos dentro de Git tales como fusionar o hacer rebase.

Después, tuviste una vista general extensiva sobre fusionar con Git. Aprendiste que la fusión es el proceso de combinar los cambios recientes de varias ramas en una sola nueva confirmación. La nueva confirmación tiene múltiples antecesores - esas confirmaciones han sido las puntas de las ramas que fueron fusionadas. En la mayoría de los casos, la fusión combina los cambios de dos ramas, y entonces la confirmación de fusión resultante tiene dos predecesores - uno de cada rama.

Consideramos una fusión sencilla y directa, la cual es posible cuando una rama difiere de la rama base, y luego agregamos confirmaciones por encima de la rama base.

Después consideramos fusiones de tres vías, y explicamos el proceso de tres etapas:

  • Primero, Git localiza la base de fusión. Como recordatorio, este es la primera confirmación que es alcanzable desde ambas ramas.
  • Segundo, Git calcula dos diffs - un diff desde la base de fusión a la primer rama, y otro diff desde la base de fusión a la segunda rama. Git genera parches basados en esos diffs.
  • Tercero y último, Git aplica ambos parches a la base de fusión usando un algoritmo de fusión de 3 vías. El resultado es el estado de la nueva confirmación de fusión.

Viste la salida de git diff cuando estamos en un estado de conflicto, y cómo resolver conflictos sea de forma manual o con VS Code.

Por último, aprendiste Git rebase. Viste que git rebase es poderoso - pero también es bastante sencillo una vez que entiendes lo que hace. Es una herramienta para "copiar-pegar" confirmaciones (o, más específicamente, parches).

Comparing rebase and merge
Comparing rebase and merge

Ambos git merge y git rebase se usan para integrar cambios introducidos en diferentes historiales.

Aunque, difieren en cómo operan. Mientras que la fusión resulta en un historial divergente, los resultados de hacer rebase resulta en un historial linear. git rebase cambia el historial, mientras que git merge agrega al historial existente.

Con este entendimiento profundo de diffs, parches, fusión y rebase, deberías sentirte confiado al introducir cambios a un repositorio git.

La próxima parte se enfocará en lo que sucede cuando las cosas salen mal - cómo puedes cambiar el historial (con o sin git rebase), o encontrar confirmaciones "perdidas".

Parte 3 - Deshacer cambios

Alguna vez llegaste a un punto donde dijiste: "Oh-oh, ¿qué es lo que hice?" Supongo que lo hiciste, así como cualquiera que usa Git.

Tal vez confirmaste a la rama equivocada. Tal vez perdiste algo de código que has escrito. Tal vez confirmaste algo que no era tu intención de hacer.

Esta parte te dará las herramientas para reescribir el historial con confianza, de esa forma "deshacer" todo tipos de cambios en Git.

Así como las otras partes del libro, esta parte será practica aunque a profundidad - así que en vez de proveerte una lista de qué a hacer cuando las cosas salen mal, entenderemos los mecanismos subyacentes, así te sentirás confiable cuando sea que llegues al momento "oh-oh". En realidad, encontrarás estos momentos como oportunidades para un desafió interesante, en vez de un escenario espantoso.

Capítulo 9 - Git Reset

Nuestro trayecto comienza con un comando potente que puede ser usado para deshacer muchas diferentes acciones con Git - git reset.

Un pequeño recordatorio - Registrando cambios

En el capítulo 3, aprendiste cómo registrar cambios en Git. Si recuerdas todo de esta parte, puedes saltar la próxima sección.

Es útil imaginarse a Git como un sistema para registrar copias instantáneas de un sistema de archivos en el tiempo. Considerando un repositorio de Git, tiene tres "estados" o "árboles":

  1. El directorio de trabajo, un directorio que tiene un repositorio asociado.
  2. El área de preparación (índice) el cual tiene el árbol para la próxima confirmación.
  3. El repositorio, el cual es una colección de confirmaciones y referencias.
The three "trees" of a Git repo
The three "trees" of a Git repo

Fíjate que con respecto a las convenciones de dibujo que utilizo: incluyo .git dentro del directorio de trabajo, para recordarte es una carpeta dentro de la carpeta del proyecto en el sistema de archivos. La carpeta .git en realidad contiene los objetos y las referencias del repositorio, como se explicó en el capítulo 4.

Demostración práctica

Usa git init para inicializar un nuevo repositorio. Escribe algo de texto en un archivo llamado 1.txt:

mkdir my_repo
cd my_repo
git init
echo Hello world > 1.txt

De los tres estados descritos arriba, ¿dónde está 1.txt ahora?

En el árbol de trabajo, ya que todavía no ha sido introducido al índice.

The file  is now a part of the working dir only
The file 1.txt is now a part of the working dir only

Para ponerlo en área de preparación, para agregarlo al índice, usa:

git add 1.txt
Using  stages the file so it is now in the index as well
Using git add stages the file so it is now in the index as well

Fíjate que una vez que pones en el área de preparación 1.txt, Git crea un objeto blob con el contenido de este archivo, y lo agrega a la base de datos de objetos interno (dentro de la carpeta .git), como se cubrió en el capítulo 3 y capítulo 4. No lo dibujo como parte del "repositorio" ya que en esta representación, el "repositorio" se refiere a un árbol de confirmaciones y sus referencias, y este blob no ha sido parte de ninguna confirmación.

Ahora, usa git commit para confirmar tus cambios al repositorio:

git commit -m "Commit 1"
Using  creates a commit object in the repository
Using git commit creates a commit object in the repository

Creaste un nuevo objeto de confirmación, el cual incluye un puntero a un árbol que describe todo el árbol de trabajo. En este caso, este árbol consiste solamente de 1.txt dentro de la carpeta raíz. Además de un puntero al árbol, el objeto de confirmación incluye metadatos, tales como marcas de tiempo e información del autor.

Cuando se considera los diagramas, fíjate que solamente tenemos una sola copia del archivo 1.txt en el disco, y un objeto blob correspondiente en la base de datos de objetos de Git. El árbol del "repositorio" ahora muestra este archivo ya que es parte de la confirmación activa - eso es, el objeto de confirmación "Commit 1" apunta a un árbol que apunta al blob con los contenidos de 1.txt, el mismo blob al que el índice está apuntando.

Para más información sobre los objetos en Git (tales como confirmaciones y árboles), ve al capítulo 1.

Luego, crea un nuevo archivo, y agrégalo al índice, como antes:

echo second file > 2.txt
git add 2.txt
The file  is in the working dir and the index after staging it with
The file 2.txt is in the working dir and the index after staging it with git add

Luego, confirma:

git commit -m "Commit 2"

Muy importante, git commit hace dos cosas:

Primero, crea un objeto de confirmación, así que hay un objeto dentro de la base de datos de objetos interno de Git con un valor SHA-1 correspondiente. Este nuevo objeto de confirmación también apunta a la confirmación antecesor. Ese es la confirmación al que HEAD estaba apuntando cuando escribiste el comando git commit.

A new commit object has been created, at first —  still points to the previous commit
A new commit object has been created, at first - main still points to the previous commit

Segundo, git commit mueve el puntero de la rama activa – en nuestro caso, eso sería main, a que apunte al objeto de confirmación creado recientemente.

 also updates the active branch to point to the newly created commit object
git commit also updates the active branch to point to the newly created commit object

Introduciendo git reset

Ahora aprenderás cómo revertir el proceso de introducir una confirmación. Para eso, aprenderás el comando git reset.

git reset --soft

El último paso que hiciste antes fue git commit, el cual en realidad significa dos cosas – Git creó un objeto de confirmación y movió a main, la rama activa. Para deshacer este paso, usa el siguiente comando:

git reset --soft HEAD~1

La sintaxis HEAD~1 se refiere al primer antecesor de HEAD. Considera un caso donde tenía más de una confirmación en el gráfico de confirmaciones, digamos que "Commit 3" está apuntando a "Commit 2", el cual, en sí, está apuntando a "Commit 1". Y considera que HEAD estaba apuntando a "Commit 3". Podrías usar HEAD~1 para referirte a "Commit 2", y HEAD~2 se referiría a "Commit 1".

Así que, devuelta al comando: git reset --soft HEAD~1.

Este comando le pide a Git que cambie cualquier HEAD al que está apuntando. (Nota: En los diagramas de abajo, uso *HEAD para "cualquier HEAD al que está apuntando".) En nuestro ejemplo, el HEAD está apuntando a main. Así que Git solamente cambiará el puntero de main a que apunte a HEAD~1. Eso es, main apuntará al "Commit 1".

Sin embargo, este comando no afectará el estado del índice o el árbol de trabajo. Así que si usas git status verás que 2.txt está en el área de preparación, justo como antes que ejecutaste git commit:

 shows that  is in the index, but not in the active commit
git status shows that 2.txt is in the index, but not in the active commit

El estado es ahora:

Resetting  to "Commit 1"
Resetting main to "Commit 1"

(Nota: quité 2.txt del "repositorio" en el diagrama ya que no es parte de la confirmación activa - eso es, el árbol apuntado por el "Commit 1" no referencia a este archivo. Sin embargo, no ha sido quitado del sistema de archivos - ya que existe en el árbol de trabajo y el índice.)

¿Qué hay sobre git log? Comenzará desde el HEAD, va al main, y luego a "Commit 1":

The output of
The output of git log

Fíjate que esto significa que "Commit 2" ya no es alcanzable desde nuestro historial.

¿Eso significa que el objeto de confirmación de "Commit 2" se elimina?

No, no se elimina. Todavía residen dentro de la base de datos de objetos interno de Git.

Si subes el historial actual ahora, usando git push, Git no empujará el "Commit 2" al servidor remoto (ya que no es alcanzable desde el HEAD actual), pero el objeto de confirmación todavía existe en tu copia local del repositorio.

Ahora, confirma nuevamente - y use el mensaje de confirmación de "Commit 2.1" para diferenciar este nuevo objeto desde el "Commit 2" original:

git commit -m "Commit 2.1"

Este el estado resultante:

Creating a new commit
Creating a new commit

Omití el "Commit 2" ya que no es alcanzable desde el HEAD, aunque su objeto existe en la base de datos de objetos interno de Git.

¿Por qué "Commit 2" y "Commit 2.1" son distintos? Inclusive si usáramos el mismo mensaje de confirmación, y aunque apunten al mismo objeto de árbol (de la carpeta raíz consistiendo de 1.txt y 2.txt), todavía tienen distintas marcas de tiempo, ya que fueron creados en diferentes tiempos. Ambos "Commit 2" y "Commit 2.1" ahora apuntan a "Commit 1", sino que solamente "Commit 2.1" es alcanzable desde el HEAD.

git reset --mixed

Es tiempo de deshacer aún mas allá. Esta vez, usa:

git reset --mixed HEAD~1

(Nota: --mixed es el conmutador predeterminado para git reset.)

Este comando comienza de la misma forma que git reset --soft HEAD~1. Eso es, el comando toma el puntero de cualquier HEAD que está apuntando ahora, el cual es la rama main, y lo establece a HEAD~1, en nuestro ejemplo - "Commit 1".

The first step of  is the same as
The first step of git reset --mixed is the same as git reset --soft

Luego, Git va más allá, efectivamente deshaciendo los cambios que hicimos al índice. Eso es, cambiar el índice para que coincida con el HEAD actual, el nuevo HEAD después de establecerlo en el primer paso.

Si ejecutáramos git reset --mixed HEAD~1, entonces el HEAD (main) sería puesto al HEAD~1 ("Commit 1"), y luego Git coincidiría el índice al estado de "Commit 1" - en este caso, significa que 2.txt ya no sería parte del índice.

The second step of  is to match the index with the new
The second step of git reset --mixed is to match the index with the new HEAD

Es tiempo de crear una nueva confirmación con el estado del "Commit 2" original. Esta vez necesita poner en el área de preparación a 2.txt nuevamente antes de crearlo:

git add 2.txt
git commit -m "Commit 2.2"
Creating "Commit 2.2"
Creating "Commit 2.2"

De forma similar a "Commit 2.1", "nombro" a esta confirmación "Commit 2.2" para diferenciarlo del del "Commit 2" original o "Commit 2.1" - estas confirmaciones resultan en el mismo estado que el "Commit 2" original, pero son distintos objetos de confirmación.

git reset --hard

Continúa, ¡deshaz aún más!

Esta vez, usa el conmutador --hard, y ejecuta:

git reset --hard HEAD~1

Una vez más, Git comienza con el paso --soft, estableciendo cualquier HEAD que esté apuntando a (main), a que apunte a HEAD~1 ("Commit 1").

The first step of  is the same as
The first step of git reset --hard is the same as git reset --soft

Luego, moviéndonos al siguiente paso --mixed, coincidiendo con el índice con HEAD. Eso es, Git deshace el paso de 2.txt.

The second step of  is the same as
The second step of git reset --hard is the same as git reset --mixed

Luego viene el paso de --hard, donde Git va mas allá y coincide con el directorio de trabajo con la etapa del índice. En este caso, significa también quitar 2.txt del directorio de trabajo.

The third step of  matches the state of the working dir with that of the index
The third step of git reset --hard matches the state of the working dir with that of the index

(Nota: en este caso específico, el archivo no está registrado, así que no será eliminado del sistema de archivos; no es realmente importante para entender git reset.)

Así que para introducir un cambio en Git, tienes tres pasos: cambias el directorio de trabajo, el índice, o el área de preparación, y luego confirmas una nueva copia instantánea con esos cambios. Para deshacer estos cambios:

  • Si usamos git reset --soft, deshacemos el último paso.
  • Si usamos git reset --mixed, también deshacemos el paso del copia instantánea.
  • Si usamos git reset --hard, deshacemos los cambios al directorio de trabajo.
The three main switches of
The three main switches of git reset

Escenarios de la vida real

Escenario #1

Así que en un escenario de la vida real, escribe "I love Git" en un archivo (love.txt), ya que todos amamos Git 😍. Adelante, pónlo en el área de preparación y confirmas esto también:

echo I love Git > love.txt
git add love.txt
git commit -m "Commit 2.3"
Creating "Commit 2.3"
Creating "Commit 2.3"

También, guarda una etiqueta así puedes volver a esta confirmación más tarde si es necesario:

git tag scenario-1

Oh, oops!

En realidad, no quería que lo confirmaras.

Lo que en realidad quería que hicieras era escribir algunas otras palabras lindas en este archivo antes de confirmarlo.

¿Qué puedes hacer?

Bueno, una forma de superar esto sería usar git reset --mixed HEAD~1, efectivamente deshaciendo tanto la confirmación y las acciones del área de preparación que tomaste:

git reset --mixed HEAD~1
Undoing the staging and committing steps
Undoing the staging and committing steps

Así que main apunta a "Commit 1" nuevamente, y love.txt ya no es una parte del índice. Sin embargo, el archivo permanece en el directorio de trabajo. Ahora puedes agregarle mas contenido:

echo and Gitting Things Done >> love.txt
Adding more love lyrics
Adding more love lyrics

Pónlo en el área de preparación y confirma tu archivo:

git add love.txt
git commit -m "Commit 2.4"
Introducing "Commit 2.4"
Introducing "Commit 2.4"

¡Bien hecho!

Lo tienes claro, lindo historial de "Commit 2.4" apuntando a "Commit 1".

Ahora tienes una nueva herramienta en tu caja de herramientas, git reset.

Esta herramienta es super, super útil, y puedes logar casi cualquier cosa con él. No es siempre la herramienta más conveniente, pero es capaz de resolver casi cualquier escenario de reescritura de historial si lo usas con cuidado.

Para principiantes, te recomiendo usar solamente git reset para casi cualquier vez que quieras deshacer en Git. Una vez que te sientas cómodo con él, continúa con otras herramientas.

Escenario #2

Consideremos otro caso.

Crea un nuevo archivo llamado new.txt; pónlo en el área de preparación y confirma:

echo this is a new file > new.txt
git add new.txt
git commit -m "Commit 3"
Creating  and "Commit 3"
Creating new.txt and "Commit 3"

(Nota: En el dibujo omití los archivos del repositorio para evitar desorden. Commit 3 incluye 1.txt, love.txt y new.txt en esta etapa.)

Ups. En realidad, eso es un error. Estabas en main, y quería que crearas esta confirmación en una rama feature. Mi culpa 😇

Hay dos herramientas mas importantes que quiero que tomes de este capítulo. El segundo es git reset. El primero y el más importante es limpiar el estado actual versus el estado en el que quieres que esté.

Para este escenario, el estado actual y el estado deseado lucen así:

Scenario #2: current-vs-desired states
Scenario #2: current-vs-desired states

(Nota: En las diagramas siguientes, me referiré al estado actual como el estado "original" - antes de empezar el proceso de reescribir el historial.)

Notarás tres cambios:

  1. main apunta a "Commit 3" (el azul) en el estado actual, pero apunta a "Commit 2.4" en el estado deseado.
  2. feature_branch no existe en el estado actual, aunque existe y apunta al "Commit 3" en el estado deseado.
  3. HEAD apunta a main en el estado actual, y apunta a feature_branch en el estado deseado.

Si lo puedes dibujar y sabes cómo usar git reset, puedes definitivamente salir de esta situación.

Así que nuevamente, lo más importante es tomar un respiro y dibujarlo.

Observando el dibujo de arriba, ¿cómo llegas al estado deseado del estado actual?

Hay unas pocas formas por supuesto, pero te presentaré una opción solamente para cada escenario. Siéntete libre de jugar con otras opciones también.

Puedes comenzar usando git reset --soft HEAD~1. Esto pondría a main a que apunte a la confirmación previa, "Commit 2.4":

git reset --soft HEAD~1
Changing ; "Commit 3 is still there, just not reachable from
Changing main: "Commit 3" is still there, just not reachable from HEAD

Mirando el diagrama actual-vs-deseado (original-vs-desired) nuevamente, puedes ver que necesitas una nueva rama, ¿no? Puedes usar git switch -c feature_branch para eso, o git checkout -b feature_branch (el cual hace lo mismo):

git switch -c feature_branch
Creating  branch
Creating feature_branch branch

Este comando también actualiza el HEAD a que apunte a la nueva rama.

Ya que usaste git reset --soft, no cambiaste el índice, así que actualmente tiene exactamente el estado que quieres confirmar - ¡qué conveniente! Puedes simplemente confirmar a feature_branch:

git commit -m "Commit 3.1"
Committing to  branch
Committing to feature_branch branch

Y llegas al estado deseado.

Escenario #3

¿Listo para aplicar tus conocimientos para casos adicionales?

Todavía en feature_branch, agrega algunos cambios a love.txt, y crea un nuevo archivo llamado cool.txt. Pónlos en el área de preparación y confirma:

echo Some changes >> love.txt
echo Git is cool > cool.txt
git add love.txt
git add cool.txt
git commit -m "Commit 4"
The history, as well as the state of the index and the working dir after creating "Commit 4"
The history, as well as the state of the index and the working dir after creating "Commit 4"

Oh, ups, en realidad quería que crearas dos confirmaciones separadas, una con cada cambio...

¿Quieres intentar este por ti mismo (antes de seguir leyendo)?

Puedes deshacer los pasos de confirmación y del área de preparación:

git reset --mixed HEAD~1

Siguiendo este comando, el índice ya no incluye esos dos cambios, pero todavía están en tu sistema de archivos:

Resulting state after using
Resulting state after using git reset --mixed HEAD~1

Así que ahora, si solamente pones en el área de preparación a love.tx, puedes confirmarlo de forma separada:

git add love.txt
git commit -m "Love"
Resulting state after committing the changes to
Resulting state after committing the changes to love.txt

Luego, haz lo mismo para cool.txt:

git add cool.txt
git commit -m "Cool"
Committing separately
Committing separately

¡Genial!

Escenario #4

Para limpiar el estado, cambia a main y usa reset --hard para hacer que apunte a "Commit 3.1", mientras pones el índice y el directorio de trabajo al estado de "Commit 3.1":

git checkout main
git reset --hard <SHA_OF_COMMIT_3_1>
Resetting  to "Commit 3.1"
Resetting main to "Commit 3.1"

Crea otro archivo (another.txt) con algo de texto, y agrega algo de texto a love.txt. Pónlos en el área de preparación ambos cambios, y confirmarlos:

echo Another file > another.txt
echo More love >> love.txt
git add another.txt
git add love.txt
git commit -m "Commit 4.1"

Este debería ser el resultado:

A new commit
A new commit

Ups...

Así que esta vez, quería que esté en otra rama, pero no una nueva rama, sino - una rama ya existente.

Así que, ¿qué puedes hacer?

Te daré una pista. La respuesta es realmente corta y fácil. ¿Qué hacemos primero?

No, no reset. Dibujemos. Esto es lo primero en hacer, ya que haría todo lo demás más fácil. Así que este es el estado actual:

The new commit on  appears blue
The new commit on main appears blue

¿Y el estado deseado?

We want the "blue" commit to be on another, , branch\label{fig-scenario-4-1}
We want the "blue" commit to be on another, existing, branch

¿Cómo obtienes el estado actual al estado deseado, qué sería más fácil?

Una forma sería usar git reset como hiciste antes, pero hay otra forma que me gustaría intentar.

Fíjate que los siguientes comandos en sí asumen que la rama existing existe en tu repositorio, aunque no lo has creado anteriormente. Para coincidir un estado donde esta rama existe, puedes usar los siguientes comandos:

git checkout <SHA_OF_COMMIT_1>
git checkout -b existing
echo "Hello" > x.txt
git add x.txt
git commit -m "Commit X"
git checkout <SHA_OF_COMMIT_3_1> -- love.txt
git commit -m "Commit Y"
git checkout main

(El comando git checkout <SHA_OF _COMMIT_3_1> -- love.txt copia los contenidos de love.txt desde "Commit 3.1" al  índice y el directorio de trabajo, así puedes confirmarlo en la rama existing. Necesitamos el estado de love.txt en el "Commit Y" que sea lo mismo que "Commit 3.1" para evitar conflictos.)

Ahora tu historial debería coincidir con el que se muestra en la imagen con el subtítulo "Queremos que la confirmación "blue" esté en otra rama, existing".

Primero, haz que el HEAD apunte a la rama existente:

git switch existing
Switch to the  branch
Switch to the existing branch

Intuitivamente, lo que quieres hacer es tomar los cambios introducidos en el "Commit 4.1", y aplicar estos cambios ("copiar-pegar") por encima de la rama exisitng. Y Git tiene una herramienta para eso.

Para pedirle a Git que tome los cambios introducidos entre una confirmación y su confirmación antecesor y sólo aplicar estos cambios en la rama activa, puedes usar git cherry-pick, un comando que introducimos en el capítulo 8. Este comando toma los cambios introducidos en la versión específicada y aplicarlos al estado de la confirmación activa. Ejecuta:

git cherry-pick <SHA_OF_COMMIT_4_1>

Puedes especificar el identificador SHA-1 de la confirmación deseada, pero también puedes usar git cherry-pick main,  como la confirmación cuyos cambios estás aplicando es al que está apuntando main.

git cherry-pick también crea un nuevo objeto de confirmación, y actualiza la rama activa a que apunte a este nuevo objeto, así que el estado resultante sería:

The result after using
The result after using git cherry-pick

Marco a la confirmación como "Commit 4.2" ya que tiene una marca de tiempo, un antecesor y un valor SHA-1 distinto de "Commit 4.1", aunque los cambios que introducen son los mismos.

Hiciste buen progreso - la confirmación deseada está ahora en la rama ¡existing! Pero no queremos que estos cambios existan el rama main.  git cherry-pick solamente aplicó los cambios a la rama existente. ¿Cómo puedes quitarlos del main?

Una forma sería volver a main, y luego hacer reset:

git switch main
git reset --hard HEAD~1

Y el resultado:

The resulting state after resetting
The resulting state after resetting main

¡Lo hiciste!

Fíjate que git cherry-pick en realidad calcula la diferencia entre la confirmación especificada y su antecesor, y luego aplica la diferencia a la confirmación activa. Esto significa que a veces, Git no será capaz de aplicar esos cambios debido a un conflicto.

También, fíjate que puedes pedirle a Git que haga cherry-pick de los cambios introducidos en cualquier confirmación, no solamente las confirmaciones referenciadas por una rama.

Recapitulación - Git Reset

En este capítulo, aprendimos cómo git reset opera, y clarificó sus tres modos principales de operación:

  • git reset --soft <commit>, el cual cambia cualquier HEAD al que esté apuntando - a <commit>.
  • git reset --mixed <commit>, el cual pasa por la etapa --soft, y también establece el estado del índice para que coincida con el de HEAD.
  • git reset --hard <commit>, el cual pasa por las etapas --soft y --mixed, y luego pone el estado del directorio de trabajo para que coincide con el del índice.

Luego aplica tus conocimientos sobre git reset para que resuelva algunos problemas de la vida real que surgen cuando se usa Git.

Al entender la forma en que Git opera, y al limpiar el estado actual versus el estado deseado, puedes abarcar con toda confianza todo tipo de escenarios.

En capítulos futuros, cubriremos comandos de Git adicionales y cómo nos pueden ayudar a resolver todo tipos de situaciones no deseadas.

Capítulo 10 - Herramientas adicionales para deshacer cambios

En el capítulo previo, conociste a git reset. De hecho, git reset es una herramienta super poderosa, y te recomiendo mucho usarlo hasta que te sientas completamente confiado con él.

Aunque, git reset no es la única herramienta a nuestra disposición. Algunas veces, no es la herramienta más conveniente para usar. En otras veces, no es suficiente. Este corto capítulo abordo algunas herramientas que son útiles para deshacer cambios en Git.

git commit --amend

Considera el Escenario #1 del capítulo anterior nuevamente. Como recordatorio, escribiste "I love Git" en un archivo (love.txt), lo pusiste en el área de preparación y confirmaste este archivo:

image-52
The state after creating "Commit 2.3"

Y luego me di cuenta que no quería que lo confirmaras a ese estado, sino - que escribieras algunas palabras de amor en este archivo antes de confirmarlo.

Para coincidir con este estado, simplemente revisa la etiqueta que creaste, el cual apunta a "Commit 2.3":

git checkout scenario-1

En el capítulo previo, cuando introdujimos git reset, resolviste este error usando git reset --mixed HEAD~1, deshaciendo efectivamente las acciones de confirmación y las acciones puesto en el área de preparación que hiciste.

Ahora me gustaría considerar otro enfoque. Continúa trabajando en el estado de la última confirmación introducida ("Commit 2.3", referenciado por la etiqueta "scenario-1"), y haz los cambios que quieras:

echo And I love this book >> love.txt

Agrega este cambio al índice:

git add love.txt

Ahora, puedes usar git commit con el conmutador --ammend, el cual le dice que sobreescriba la confirmación al que HEAD está apuntando. En realidad, creará otra nueva confirmación, que apunta a HEAD~1 ("Commit 1" en nuestro ejemplo), y hará que HEAD apunte a esta nueva confirmación creada. Proveyendo el argumento -m puedes especificar un nuevo mensaje de confirmación también:

git commit --amend -m "Commit 2.4"

Después de ejecutar este comando, HEAD apunta a main, el cual apunta a "Commit 2.4", el cual en sí apunta a "Commit 1". El "Commit 2.3" previo ya no es alcanzable desde el historial.

commit_amend-1
The state after using git commit --amend (Commit "2.3" is unreachable and thus not included in the drawing)

Esta herramienta es útil cuando quieras sobrescribir rápidamente la última confirmación que creaste. De hecho, podrías usar git reset para lograr lo mismo, pero puedes ver git commit --ammend como un atajo más conveniente.

git revert

Muy bien, otro día, otro problema.

Agrega el siguiente texto a love.txt, pónlo en el área de preparación y confirma como sigue:

echo This is more tezt >> love.txt
git add love.txt
git commit -m "Commit 3"
Committing "More changes"
The state after committing "Commit 3"

Y empújalo al servidor remoto:

git push origin HEAD

Am, ups 😓…

Noté algo. Tuve un error de tipado aquí. Escribí "This is more tezt" en vez de "This is more text". Woops. Así que, ¿cuál es el problema grande ahora? Hice push, lo que significa que alguien más ya podría haber hecho push de esos cambios.

Si sobrescribo esos cambios usando git reset, tendremos historiales distintos, y todo el infierno podría desatarse. Puedes sobrescribir tu propia copia del repo  tanto como quieras hasta que hagas push.

Una vez que hagas push del cambio, necesitas estar seguro que nadie más ha solicitado esos cambios si vas a reescribir el historial.

De forma alternativa, puedes usar otra herramienta llamada git revert. Este comando toma la confirmación que le estás proveyendo y calcula el diff de su confirmación antecesor, así como git cherry-pick, pero esta vez, calcula los cambios inversos. Eso es, si en la confirmación especificada agregaste una línea, el servidor eliminaría la línea, y vice versa.

En nuestro caso estamos revirtiendo "Commit 3", así que la reversa sería eliminar la línea "This is more tezt" de love.txt. Ya que "Commit 3" está referenciado por main y HEAD, podemos usar cualquier de las referencias nombradas en este comando:

Using  to undo the changes
Using git revert to undo the changes

git revert creó un nuevo objeto de confirmación, lo cual significa que es una adición al historial. Al usar git revert, no reescribiste el historial. Admitiste tu error pasado, y esta confirmación es un reconocimiento que cometiste un error y ahora lo arreglaste.

Alguno diría esta es la forma más madura. Alguno diría que no es un historial tan limpio como lo obtendrías si usaras git reset para reescribir la confirmación previa. Pero este es una forma de evitar reescribir el historial.

Ahora puedes arreglar el error de tipado y confirmar nuevamente:

echo This is more text >> love.txt
git add love.txt
git commit -m "Commit 3.1"
Redoing the changes
The resulting state after redoing the changes

Puedes usar git revert para revertir una confirmación que no sea HEAD. Digamos que quieres revertir el antecesor de HEAD, puedes usar:

git revert HEAD~1

O podrías proveer el SHA-1 de la confirmación para revertir.

Fíjate ya que Git aplicará el parche de reversa del parche anterior - esta operación podría fallar, ya que el parche ya no se podría aplicar y tendrías un conflicto.

Git Rebase como una herramienta para deshacer cosas

En el capítulo 8, aprendiste sobre Git rebase. Lo consideramos principalmente como una herramienta para combinar cambios introducidos en diferentes ramas. Sin embargo, siempre y cuando no hayas hecho push de tus cambios, usando rebase en tu propia rama puede ser una forma muy conveniente para reordenar tu historial de confirmaciones.

Para ello, usualmente harías rebase en una sola rama, y usa el rebase interactivo. Considera nuevamente este ejemplo abarcado en el capítulo 8, donde trabajé desde feature_branch_2, y específicamente edité el archivo code.py. Comencé por cambiar todas las cadenas para que sean envueltas por comillas dobles en vez de comillas simples:

Changing  into  in
Changing ' into " in code.py

Luego, lo puse en el área de preparación y confirmé:

git add code.py
git commit -m "Commit 17"

Luego decidí agregar una nueva función al principio del archivo:

Adding the function
Adding the function another_feature

Nuevamente, lo puse en el área de preparación y confirmé:

git add code.py
git commit -m "Commit 18"

Y ahora me di cuenta que en realidad me olvidé de cambiar las comillas simples a comillas dobles al envuelto de __main__ (como lo has visto), así que hice eso también:

Changing  into
Changing '__main__' into "__main__"

Por supuesto, lo puse en el área de preparación y confirmé este cambio:

git add code.py
git commit -m "Commit 19"

Ahora, considera el historial:

The commit history after introducing "Commit 19"
The commit history after introducing "Commit 19"

Como expliqué en el capítulo 8, obtuve un estado con dos confirmaciones que están relacionados uno con el otro, "Commit 17" y "Commit 19" (cambiando ' a "), pero están separados por el "Commit 18" que no está relacionado (donde agregué una nueva función).

Este es un caso básico donde git rebase sería práctico, para deshacer los cambios locales antes de hacer push de un historial limpio.

Intuitivamente, quiero editar el historial aquí:

These are the commits I want to edit
These are the commits I want to edit

Puedo hacer rebase del historial desde "Commit 17" a "Commit 19", por encima de "Commit 15". Para hacer eso:

git rebase --interactive --onto <SHA_OF_COMMIT_15> <SHA_OF_COMMIT_15>
Using  on a single branch
Using rebase --onto on a single branch

Esto resulta en la siguiente pantalla:

Interactive rebase
Interactive rebase

Así que, ¿qué haría? Quiero poner a "Commit 19" antes de "Commit 18", así viene justo después de "Commit 17". Puedo ir más lejos y hacerles squash ("aplastarlos", de manera figurativa), así:

Interactive rebase - changing the order of commit and squashing
Interactive rebase - changing the order of commit and squashing

Ahora cuando recibo una pantalla para escribir un mensaje de confirmación, puedo proveer el mensaje "Commit 17+19":

Providing a commit message
Providing a commit message

Y ahora, vemos nuestro precioso historial:

The resulting history
The resulting history

La sintaxis usada arriba, git rebase --interactive --onto <COMMIT X> <COMMIT X> sería la sintaxis más usada comúnmente por aquellos que usan rebase regularmente. La manera de pensar que estos desarrolladores usualmente tienen es crear confirmaciones atómicas mientras trabajan, todo el tiempo, sin asustarse de cambiarlos luego. Entonces, antes de hacer push de sus cambios, harían rebase a todo el conjunto de cambios desde el último push, y re-arreglarlo así el historial se vuelve coherente.

git reflog

Es tiempo de considerar un caso más alarmante.

Vuelve al "Commit 2.4":

git reset --hard <SHA_OF_COMMIT_2_4>

Haz algo, escribe algo de código, y agrégalo a love.txt. Pónlo en el área de preparación a este cambio, y confírmalo:

echo lots of work >> love.txt
git add love.txt
git commit -m "Commit 3.2"

(Estoy usando "Commit 3.2" para indicar que este no es la misma confirmación que "Commit 3" que usamos cuando explicamos sobre git revert.)

Another commit
Another commit - "Commit 3.2"

Hice lo mismo en mi máquina, y usé la tecla flecha arriba Up en mi teclado para volver a los comandos anteriores, y luego presioné Enter, y... Wow.

Whoops.

Did I just ?
Did I just git reset -- hard?

¿Acaso usé git reset --hard? 😨

¿Pero qué pasó en realidad? Como aprendiste en el capítulo previo, Git movió el puntero a HEAD~1, de esa forma la última confirmación, con todo mi trabajo precioso, no es alcanzable desde el historial actual. Git también quitó todos los cambios desde el área de preparación, y luego hice coincidir el directorio de trabajo con el del estado del área de preparación.

Eso es, todo coincide con este estado donde mi trabajo... se ha ido.

Momento de terror. Me voy asustando.

Pero, realmente, ¿hay una razón para asustarse? No realmente... Somos personas relajadas. ¿Qué hacemos? Bueno, intuitivamente, ¿realmente, realmente se ha ido la última confirmación?

No. ¿Y por qué no? Todavía existe dentro de la base de datos interno de Git.

Si tan solo supiera dónde está, sabría el valor SHA-1 que identifica a esta confirmación, y podríamos restaurarlo. Inclusive podría deshacer lo deshecho, y hacer reset y volver a esta confirmación.

En realidad, lo único que realmente necesito aquí es el SHA-1 de la confirmación "eliminada".

Ahora la pregunta es, ¿cómo lo encuentro? ¿git log sería útil?

Bueno, no realmente. git log iría al HEAD, el cual apunta a main, el cual apunta a la confirmación antecesora de la confirmación que estamos buscando. Luego, git log recorrería a través de la cadena de antecesores, el cual no incluye la confirmación con mi trabajo precioso.

 doesn't help in this case
git log doesn't help in this case

Afortunadamente, los genios que crearon Git también crearon un plan de recuperación para nosotros, y se llama el reflog.

Mientras trabajas con Git, cuando sea que cambies el HEAD, lo cual lo puedes hacer usando git reset, pero también con otros comandos como git switch o git checkout, Git agrega una entrada al reflog.

 shows us where  was
git reflog shows us where HEAD was

¡Encontramos nuestra confirmación! Es el que comienza con 0fb929e.

También podemos relacionarlo por su "nickname" - HEAD@{1}. De forma similar Git usa al HEAD~1 para obtener el primer antecesor de HEAD, y HEAD~2 para que se refiera al segundo antecesor de HEAD, y así sucesivamente, Git usa HEAD@{1} para referirse al primer reflog antecesor de HEAD, eso es, a donde apuntaba HEAD en el paso anterior.

También podemos pedirle a git rev-parse que nos muestre su valor:

Using
Using git rev-parse HEAD@{1}

Nota: En caso que estés usando Windows, podrías necesitar envolverlo con comillas - así:

git rev-parse "HEAD@{1}"

Otra forma de ver el reflog es usando git log -g, el cual le pide a git log que considere el reflog:

The output of
The output of git log -g

Puedes ver en la salida de git log -g que la entrada de reflogHEAD@{0}, justo como el HEAD, apunta a main, el cual apunta a "Commit 2". Pero el antecesor de esa entrada en el reflog apunta a "Commit 3".

Así que para volver a "Commit 3", puedes usar git reset --hard HEAD@{1} (o el valor SHA-1 de "Commit 3"):

git_reflog_reset
git reset --hard HEAD@{1}

Y ahora, si haces git log:

Our history is back!!!
Our history is back!!!

¡Nos salvamos!

¿Qué sucedería si usara este comando nuevamente? ¿Y ejecutara git reset --hard HEAD@{1}?

Git pondría al HEAD a dónde HEAD estaba apuntando antes del último reset, lo que significa a "Commit 2". Podemos continuar todo el día:

 again
git reset --hard again

Recapitulación - Herramientas adicionales para Deshacer Cambios

En el capítulo anterior, aprendiste cómo usar git reset para deshacer cambios.

En este capítulo, agrandaste tu caja de herramientas para deshacer cambios en Git con algunos comandos nuevos:

  • git commit --ammend - el cual "sobreescribe" la última confirmación con el área de preparación del índice. Mayoritariamente útil cuando confirmaste algo y quieres modificar la última confirmación.
  • git revert - el cual crea una nueva confirmación, que revierte una confirmación anterior agregando una nueva confirmación al historial con los cambios revertidos. Especialmente útil cuando la confirmación "defectuosa" ya ha sido subida al remoto.
  • git rebase - el cual ya los conocías desde el capítulo 8, y es útil para reescribir el historial de confirmaciones múltiples, especialmente antes de empujarlos.
  • git reflog (y git log -g) - el cual rastrea todos los cambios hasta el HEAD, así puedes encontrar el valor SHA-1 de una confirmación al que necesitas volver.

La herramienta más importante, inclusive más importante que las herramientas que listé, es limpiar la situación actual vs el desado. Créeme en esta, hará que cada situación parezca menos intimidante y la solución más clara.

Hay herramientas adicionales que te permiten revertir cambios en Git (proveeré enlaces en el apéndice), pero la colección de herramientas que cubrí aquí debería prepararte para abarcar cualquier desafío con confianza.

Capítulo 11 - Ejercicios

Este capítulo incluye algunos ejercicios para profundizar tus conocimientos de las herramientas que aprendiste en la Parte 3. La versión completa de este libro también incluye soluciones detalladas de cada una.

Los ejercicios se encuentran en este repositorio:

https://github.com/Omerr/undo-exercises.git

Cada ejercicio está en una rama con el nombre exercise_XX, así que el Ejercicio 1 se encuentra en la rama exercise_01, el Ejercicio 2 se encuentra en la rama exercise_02 y así sucesivamente.

Nota: como se explicó en los capítulos previos, si trabajas con confirmaciones que pueden ser encontradas en un servidor remoto (los cuales son en la que estás en este caso, ya que estás usando mi repositorio "undo-exercises"), deberías probablemente usar git revert en vez de git reset. De la misma forma con git rebase, el comando git reset también reescribe el historial - y así te refrenas de usarlo en las confirmaciones en las que otros podían basarse.

Para los propósitos de estos ejercicios, puedes asumir que nadie ha clonado o haya descargado (pull) el código del repositorio remoto. Sólo recuerda - en la vida real, probablemente deberías usar git revert en vez de comandos que reescriben el historial en tales casos.

Ejercicio 1

En la rama execise_01, considera el archivo hello.txt:

The file
The file hello.txt

Este archivo incluye un error de tipado (en el último caracter). Encuentra la confirmación que introdujo este error de tipado.

Ejercicio (1a)

Quita esta confirmación del historial alcanzable usando git reset (con los argumentos correctos), arregla el error de tipado, y confirma nuevamente. Considera tu historial.

Revierte al estado previo.

Ejercicio (1b)

Quita la confirmación defectuosa usando git commit --ammend, y vuelve al mismo estado del historial como al final del ejercicio (1a).

Revierte al estado previo.

Ejercicio (1c)

Haz revert de la confirmación defectuosa usando git revert y arregla el error de tipado. Considera tu historial.

Revierte al estado previo.

Ejercicio (1d)

Usando git rebase, vuelve al misma estado como al final del ejercicio (1a).

Ejercicio 2

Cambia a la rama exercise_02, y considera los contenidos de exercise_02.txt:

The contents of
The contents of exercise_02.txt

Un archivo sencillo, con un caracter en cada línea.

Considera el historial (usando git lol):

ex_02_2
git lol

Oh mi. Cada caracter fue introducido en una confirmación separada. ¡Eso no tiene sentido!

Usa las herramientas que has adquirido para crear un historial donde la creación de exercise_02.txt está hecho en una sola confirmación.

Ejercicio 3

Considera el historial en la rama exercise_03:

The history on
The history on exercise_03

Esto parece un desastre. Notarás que:

  • El orden está sesgado. Necesitamos que "Commit 1" sea la primera confirmación en esta rama, y tenga a "Initial Commit" como su antecesor, seguido de "Commit 2" y así sucesivamente.
  • No deberíamos tener "Commit 2a" y "Commit 2b", o "Commit 4a" y "Commit 4b" - estos dos pares necesitan ser combinados en una sola confirmación cada uno - "Commit 2" y "Commit 4".
  • Hay un error de tipado en el mensaje de confirmación de "Commit 1", no debería tener 3 m.

Arregla estos errores, pero básate en los cambios de cada confirmación original. El historial resultante debería lucir así:

The desired history
The desired history

Ejercicio 4

Este ejercicio en realidad consiste de tres ramas: exercise_04, exercise_04_a, y exercise_04_b.

Para ver el historial de estas ramas sin las otras, usa la siguiente sintaxis:

git lol --branches="exercise_04*"

El resultado es:

The output of
The output of git lol --branches="exercise_04*"

Tu objetivo es hacer que exercise_04_b sea independiente de exercise_04_a. Eso es, obtener este historial:

The desired history
The desired history

¡Buena suerte!

Parte 4 - Herramientas de Git Fantásticas y Útiles

Git tiene un montón de comandos, y estos comandos tienen muchísimas opciones y argumentos. Podría intentar cubrirlos todos (aunque cambian con el tiempo), pero no veo el punto en ello. Probablemente deberías conocer un sub-conjunto de estos comandos realmente bien, aquellos que usas regularmente. Luego, siempre puedes buscar por un comando específico para realizar una tarea a mano.

Esta parte se basa en las bases que adquiriste en las partes previas, y cubre comandos específicos y opciones que podrías encontrar útiles. Dado tu entendimiento de cómo funciona Git, tener estas pequeñas herramientas puede hacerte un verdadero pro para llevar a Git a cabo.

Capítulo 12 - Git Log

Usaste git log muchas veces a lo largo de los distintos capítulos, y probablemente lo has usado muchas veces antes de leer este libro.

La mayoría de los desarrolladores usan git log, pocos lo usan de forma efectiva. En este capítulo aprenderás ajustes útiles para sacar el mayor provecho a git log. Una vez que te sientas cómodo con los diferentes conmutadores de este comando, será un verdadero cambio en tu día a día trabajando con Git.

Pensando en ello, git log abarca la esencia de cada versión del sistema de control - es decir, para registrar cambios en las versiones. Registras versiones de esa forma puedes considerar el historial de tu proyecto - tal vez revertir o aplicar cambios específicos, preferir cambiar a un punto distinto en el tiempo y probar las cosas allí. Tal vez te gustaría saber quién contribuyó una cierta pieza de código o cuando lo hicieron.

Mientras que git sí preserva esta información usando objetos de confirmación, eso también apunta a sus confirmaciones antecesoras, y referencias a objetos de confirmación (tales como ramas o HEAD), este almacenamiento de versiones no es suficiente. Sin ser capaz de encontrar la confirmación relevante que te gustaría considerar, o reunir información relevante sobre ello, tener estos datos almacenados es bastante inútil.

Puedes imaginarte a tus objetos de confirmación como distintos libros que se apilan en un pila gigante, o en una biblioteca, llenando largos estantes. La información que podrías necesitar está en estos libros, pero si no tienes un índice - una forma para saber en qué libro yace la información que buscas, o dónde este libro se localiza dentro de la biblioteca - no serías capaz de hacer uso de él. git log es esta indexación de tu biblioteca - es una forma de encontrar las confirmaciones relevantes y la información sobre ellos.

Los argumentos útiles para git log que aprenderás en este capítulo dan formato en cómo se muestran las confirmaciones en el log, o filtran confirmaciones específicas.

git lol, un alias que he usado a lo largo de este libro, usa algunos de estos conmutadores, como lo demostraré. Siéntete libre de ajustar este alias (o crea otro desde cero) después de leer este capítulo.

Como en otros capítulos, el objetivo no es proveer una referencia completa, por lo tanto no proveeré todos los diferentes conmutadores de git lol. Me enfocaré en los conmutadores que creo que los encontrarás útiles.

Filtrando comandos

Considera la salida predeterminada de git log:

The output of  without additional switches
The output of git log without additional switches

El log comienza desde el HEAD, y sigue la cadena de antecesores.

Las Confirmaciones (no) alcanzables desde...

Cuando escribes git log <revision>, git log incluirá todas las entradas accesibles desde <revision>. Con "accesible", me refiero accesible al seguir la cadena de antecesores. Así que ejecutando git log sin argumentos es lo mismo que ejecutar git log HEAD.

Puedes especificar múltiples revisiones para git log- si escribes git log branch_1 branch_2, le pides a git log que incluya cada confirmación que es alcanzable desde branch_1 o branch_2 (o ambos).

git log excluirá cualquier confirmación que sea alcanzable desde las revisiones precedidos por un ^.

Por ejemplo, el siguiente comando:

git log branch_1 ^branch_2

le pide a git log que incluya cada confirmación que es accesible desde branch_1, pero no aquellos que son accesibles desde branch_2.

Considera el historial cuando uso git log feature_branch_2 en este repo:

git_log_2-1
git log feature_branch_1

El historial incluye todas las confirmaciones accesibles por feature_branch_1. Ya que esta rama "se creó" desde main (eso es, "Commit 12", al que apunta main, es accesible desde la cadena de antecesores) - el log también incluye todas las confirmaciones accesibles desde main.

¿Qué sucedería si ejecutara este comando?

git log feature_branch_1 ^main
git_log_3
git log feature_branch_1 ^main

En efecto, git log muestra sólo "Commit 13" y "Commit 16", los cuales son accesibles desde feature_branch_1 pero no desde main.

git log --all

Para seguir las confirmaciones que son accesibles desde cualquier referencia nombrada o (cualquier ref en refs/) o desde el HEAD.

Por Autor

Si sabes que estás buscando una confirmación que tiene una persona específica como autor, puedes filtrar estas confirmaciones usando el nombre del usuario o su email, así:

git log --author="Name"

Puedes usar expresiones regulares para buscar los nombres de los autores que coinciden con un patrón específico, por ejemplo:

git log --author="John\|Jane"

filtrará las confirmaciones cuyo autor es John o Jane.

Por Fecha

Cuando sabes que el cambio que estás buscando ha sido confirmado dentro de una fracción de tiempo específico, puedes usar --before o --after para filtrar las confirmaciones desde esa fracción de tiempo.

Por ejemplo, para obtener todas las confirmaciones introducidos después del 12 de Abril de 2023 (inclusivo), usa:

git log --after="2023-04-12"

Por Rutas

Puedes pedir a git log que sólo muestre las confirmaciones donde los cambios han sido introducidos en rutas específicas. Fíjate que no significa que cualquier confirmación que apunte a un árbol que incluya los archivos en cuestión, sino más bien que si calculamos la diferencia entre la confirmación en cuestión y sus antecesores, veríamos que al menos uno de las rutas ha sido modificado.

Por ejemplo, puedes usar:

git log --all -- 1.py

para encontrar todas las confirmaciones que son accesibles desde cualquier puntero nombrado, o HEAD, e introducir un cambio a 1.py. Puedes especificar múltiples rutas:

git log --all -- 1.py 2.py

El comando previo hará que git log incluya confirmaciones accesibles que introdujeron un cambio a 1.py o 2.py (o ambos).

También puedes usar un patrón glob, por ejemplo:

git log -- *.py

incluirá las confirmaciones que son accesibles desde el HEAD que incluye un cambio a cualquier archivo en el directorio raíz cuyo nombre termina con un .py. Para buscar un archivo que termine con .py, puedes usar:

git log -- **/*.py

Por Mensaje de Confirmación

Si conoces el mensaje de confirmación (o partes de él) de la confirmación que estás buscando, puedes usar el conmutador --grep para "git log", por ejemplo:

git log --grep="Commit 12"

devuelve la confirmación con el mensaje "Commit 12".

Por Contenido de Diff

Este es super útil, y me ha salvado incontables veces. Al usar git log -S, puedes buscar confirmaciones que introducen o quitan una línea en particular del código fuente.

Esto es práctico, por ejemplo, cuando sabes que has creado algo en el repo, pero no sabes dónde está ahora. No puedes encontrarlo en ningún lugar de tu sistema de archivos (no está en el HEAD), y sabes que debe estar ahí - acechando en algún lugar en esta librería (montón de confirmaciones) que tienes.

Digamos que recuerdo dónde escribí una línea con el texto Git is awesome, pero no puedo encontrarlo ahora. Podría ejecutar:

git log --all -S"Git is awesome"

Fíjate que usé --all para evitar limitarme a confirmaciones alcanzables desde el HEAD.

También puedes buscar una expresión regular, usando -G:

git log --all -G"Git .* awesome"

Formateando el Log

Considera la salida predeterminada de git log nuevamente:

The output of  without additional switches
The output of git log without additional switches

El log comienza desde el HEAD, y sigue la cadena de antecesores.

Cada entrada del log comienza con una línea con commit y luego el SHA-1 de la confirmación, tal vez seguida de punteros adicionales que apuntan a esta confirmación.
Luego es seguido por el autor, fecha, y mensaje de confirmación.

--oneline

La principal dificultad con la salida predeterminada de git log es que es difícil de entender un historial con más de unas pocas confirmaciones, ya que simplemente no los ves a todos.

En la salida de git log que se muestra abajo, solamente cuatro objetos de confirmación aparecieron en mi pantalla. Usando git log --oneline provee una vista más concisa, mostrando el SHA-1 de la confirmación, al lado de su mensaje, y referencia nombradas si son relevantes:

The output of
The output of git log --oneline

Si deseas omitir las referencias nombradas, puedes agregar el conmutador --no-decorate:

The output of
The output of git log --oneline --no-decorate

Para pedirle a git log explícitamente que muestre decoraciones, puedes usar git log --decorate.

--graph

git log --oneline muestra una representación compacta. Eso es genial cuando tenemos un historial linear, tal vez en una sola rama. Pero, ¿qué sucede cuando tenemos múltiples ramas, que podría diferir uno del otro?

Considera la salida del siguiente comando en mi repositorio:

git log --oneline feature_branch_1 feature_branch_2
The output of
The output of git log --oneline feature_branch_1 feature_branch_2

git log muestra cualquier confirmación accesible por feature_branch_1, feature_branch_2, o ambos. Pero, ¿cómo se ve el historial? ¿feature_branch_2 difirió de feature_branch_1? ¿O difirió de main? Es imposible distinguirlo desde esta vista.

Aquí es donde --graph viene a mano, dibujando un gráfico ASCII representando la estructura de la rama del historial de confirmaciones. Si agregamos esta opción al comando previo:

The output of
The output of git log --oneline --graph feature_branch_1 feature_branch_2

En realidad puedes ver que feature_branch_1 se creó desde main (ya que "Commit 12", main, es el antecesor de "Commit 13"), y también que feature_branch_2 se creó desde main (ya que el antecesor de "Commit 14" también es "Commit 12").

El símbolo * nos dice "en" qué rama está una cierta confirmación, así sabes con certeza que "Commit 13" está en feature_branch_1, y no en feature_branch_2.

--pretty=format

El resultado de arriba, ¡ya es muy útil! Aunque, le falta un par de cosas. No sabemos el autor o el tiempo de la confirmación. Estos dos detalles fueron incluidos en la salida predeterminada de git log el cual fue muy largo. Tal vez, ¿los podemos agregar de una forma más compacta?

Al usar --pretty=format:, puedes mostrar la información de cada confirmación en varias formas usando marcadores de posición al estilo de printf.

En el siguiente comando, los marcadores de posición %s, %an y %cd son reemplazados por el sujeto de la confirmación (mensaje), nombre de autor, y la fecha de la confirmación, respectivamente.

git log --oneline --graph feature_branch_1 feature_branch_2 --pretty=format:"%s (%an) [%cd]"

La salida luce así:

git_log_9
git log --oneline --graph feature_branch_1 feature_branch_2 --pretty=format:"%s (%an) [%cd]

Eso es útil, pero no bueno al mirarlo. Entonces podemos usar otros trucos de formateo, específicamente %C(color) que cambiará el color a color, hasta que alcance un %Creset que resetea el color. Para hacer que el nombre del autor sea amarillo, puedes usar:

git log --oneline --graph feature_branch_1 feature_branch_2 --pretty=format:"%s %C(yellow)(%an)%Creset [%cd]"
git_log_10
git log --oneline --graph feature_branch_1 feature_branch_2 --pretty=format:"%s %C(yellow)(%an)%Creset [%cd]"

Para algunos colores, como rojo o verde, es innecesario incluir el paréntesis, así que rojo es suficiente.

¿Cómo se estructura git lol?

Cuando ejecuto git lol, en realidad ejecuta lo siguiente:

git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

¿Puedes tomar esto poco a poco?

Ya conoces --graph, el cual hace que la salida incluya un gráfico ASCII.

--abbrev-commit usa un prefijo corto del SHA-1 completo de la confirmación (en mi configuración, los siete primero caracteres).

El resto es sólo el coloreamiento de varios detalles sobre la confirmación:

git lol --all
git_log_11
git lol --all

Me gusta esta salida porque lo encuentro claro. Me da la información que necesito, con colores suficientes así cada detalles se destaca sin lastimar mis ojos. Pero si prefieres otra información, otros colores, un diferente orden, o cualquier cosa - adelante y ajústalo a tu gusto.

Definiendo un alias

Como sabes, defino a git lol como un alias - eso es, cuando ejecuto git lol, ejecuta el comando largo que proveí anteriormente.

¿Cómo puedes crear un alias en Git?

La forma mas fácil es usar git alias, así:

git config --global alias.co checkout

Este comando pone co para que sea un alias para el comando checkout, así puedes usar git co main en vez de git checkout main.

Para definir git lol como un alias, puedes usar:

git config --global alias.lol 'log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit'

Capítulo 13 - Git Bisect

Ups.

Tengo un error.

Sí, sucede a veces, a todos nosotros. Algo en mi sistema está roto, y no puedo distinguir por qué. He estado depurando por un rato, pero la solución no está clara.

Puedo decir que dos semanas atrás, esto no sucedió. Afortunadamente para mí, he estado usando Git (obviamente lo sé...), así que puedo volver en el tiempo y probar una versión anterior de mi código. En efecto, en esta versión - todo funcionó bien.

Pero... he hecho muchos cambios en estas dos semanas. Ay, no solo yo - mi equipo completo ha contribuido con confirmaciones que agregan, eliminan, o modifican partes del código base. ¿Por dónde comienzo? ¿Debería ir por cada cambio introducido en esas dos semanas?

Ingresa - git bisect.

El objetivo de git bisect es ayudarte a encontrar la confirmación donde un error fue introducido, de una manera efectiva.

¿Cómo funciona git bisect?

git bisect primero te pide que marques una confirmación como "mala" (donde el error ocurre), y otra confirmación como "buena" (una sin el error). Luego, verifica una confirmación a medio camino entre estas dos confirmaciones, y luego te pide que identifiques la confirmación como "buena" o "mala". Este proceso se repite hasta que encuentres la primer confirmación "mala".

La clave aquí es usar la búsqueda binaria - mirando al punto a medio camino y decidir si es la nueva punta o el piso de la lista de confirmaciones, puedes encontrar la confirmación correcta de forma eficiente. Inclusive si tienes 10,000 confirmaciones que cazar, solamente toma un máximo de 13 pasos para encontrar la primer confirmación que introdujo el error.

Ejemplo de git bisect

Para este ejemplo, usaré el repositorio en https://github.com/Omerr/bisect-exercise.git. Para crearlo, adaptaré el repositorio de código abierto https://github.com/bast/git-bisect-exercise (acorde a su licencia).

En este repositorio, tenemos un archivo python sencillo que se usa para calcular el valor de pi (el cual es aproximadamente 3.14). Si ejecutas python3 get_pi.py en el main, sin embargo, obtendrás un resultado erróneo:

A wrong result, we have a bug
A wrong result, we have a bug

Esta rama consiste de más de 500 confirmaciones.

Encuentra la primer confirmación en esta rama usando:

git log --oneline | tail -n 1
bisect_2
git log --oneline | tail -n 1

Si ejecutas checkout para esta confirmación y ejecutas python3 get_pi.py nuevamente, el resultado es correcto:

From the first commit, the result is valid
From the first commit, the result is valid

Así que en algún lugar entre el HEAD y la confirmación f0ea950, un cambio fue introducido que resultó en esta salida errónea.

Para encontrarlo usando git bisect, comienza el proceso de bisect, y marca esta confirmación como "buena":

git bisect start
git bisect good

Por defecto, git bisect good tomaría el HEAD como la confirmación "buena". Para marcar a main como "mala", puedes usar git bisect bad main:

bisect_3
git bisect bad main

git bisect comprobó el número de confirmación 251, el "punto medio" de la rama main. ¿El estado de esta confirmación produce la saluda correcta o errónea?

Trying again...
Trying again...

Todavía obtenemos la salida errónea, lo cual significa que podemos descartar las confirmaciones 252 hasta el 500 (y confirmaciones adicionales después de ese), y acortar nuestra búsqueda desde la confirmación 2 hasta el 251. Marca a éste como "mala":

Mark as
Mark as bad

git bisect comprobó la confirmación "media" (número 126), y ejecutando el código nuevamente resulta en la respuesta, ¡correcta! Esto significa que esta confirmación es "buena", y que la primer confirmación "mala" está en algún lugar entre 127 y 251. Márcalo como "buena".

Mark as
Mark as good

Genial, git bisect nos toma a la confirmación 188, ya que esta es la confirmación "media" entre 127 y 251. Al ejecutar el código nuevamente, puedes ver que el resultado es incorrecto, así que este es en realidad una confirmación "mala", lo que significa que la primer confirmación defectuosa se encuentre en algún lugar entre 127 y 188. Como puedes ver, git bisect achica el espacio de búsqueda a la mitad en cada iteración.

Vamos, ahora es tu turno - ¡continúa desde aquí! Prueba el resultado de python3 get_pi.py y usa git bisect good o git bisect bad para marcar la confirmación según sea el caso. ¿Cuál es la confirmación defectuosa?

Cuando estés listo, usa git bisect reset para detener el proceso bisect.

git bisect automático

En el ejemplo previo, podrías simplemente ejecutar pytho3 get_pi.py y verificar el resultado. En otras veces, el proceso de validar si una cierta confirmación es "buena" o "mala" puede ser tramposo, propenso a errores, o solo lleva mucho tiempo.

Es posible automatizar el proceso de git bisect al crear código que debería ser ejecutado en cada iteración, devolviendo 0 cuando la confirmación actual es "buena", y un valor entre 1-127 (inclusivo), excepto 125, si debería ser considerada "mala".

La sintaxis es:

git bisect run my_script arguments

Ya que este libro no trata sobre programación y no se asume que conoces un lenguaje de programación específico, no mostraré un ejemplo de la implementación my_script. El archivo README.md en el repositorio usado en este capítulo (https://github.com/Omerr/bisect-exercise.git) incluye un ejemplo para un script que puedes ejecutar con git bisect run para encontrar automáticamente la confirmación defectuosa para el ejemplo previo.

Capítulo 14 - Otros comandos útiles

Este capítulo resalta algunos comandos que ya han sido mencionados en los capítulos anteriores. Los estoy poniendo aquí juntos así puedes revisarlos como una referencia cuando sea necesario.

git cherry-pick

Introducido en el capítulo 8, este comando toma una confirmación dada, calcula el parche que esta confirmación introduce al calcular la diferencia entre la confirmación del antecesor y la confirmación misma, y luego git bisect "repite" esta diferencia. Es como "copiar-pegar" una confirmación, es decir, el diff de esta confirmación introducida.

En el capítulo 8 consideramos la diferencia introducida por el "Commit 5" (usando git diff main <SHA_OF_COMMIT_5>):

Running  to observe the patch introduced by "Commit 5"
Running git diff to observe the patch introduced by "Commit 5"

Puedes ver que en esta confirmación, John comenzó a trabajar en una canción llamada "Lucy in the Sky with Diamonds":

The output of  - the patch introduced by "Commit 5"
The output of git diff - the patch introduced by "Commit 5"

Como recordatorio, también puedes usar el comando git show para obtener la misma salida:

git show <SHA_OF_COMMIT_5>

Ahora, si haces cherry-pick a esta confirmación, introducirás este cambio específicamente, en la rama activa. Puedes cambiar a la rama main:

git checkout main (or git switch main)

Y crear otra rama:

git checkout -b my_branch (or git switch -c my_branch)
Creating  that branches from
Creating my_branch that branches from main

Luego, haz cherry-pick al "Commit 5":

git cherry-pick <SHA_OF_COMMIT_5>
Using  to apply the changes introduced in "Commit 5" onto
Using cherry-pick to apply the changes introduced in "Commit 5" onto main

Considera el log (salida de git lol):

The output of
The output of git lol

Parece que copiaste-pegaste a "Commit 5". Recuerda que inclusive tiene el mismo mensaje de confirmación, e introduce los mismos cambios, e inclusive apunta al mismo objeto árbol como el "Commit 5" original en este caso - todavía es un objeto de confirmación distinto, ya que fue creado con una marca de tiempo distinto.

Mirando a los cambios, usando git show HEAD:

The output of
The output of git show HEAD

Son los mismos como los de "Commit 5".

git revert

git revert es esencialmente la reversa de git cherry-pick, introducido en el capítulo 10. Este comando toma la confirmación con el que le estás proveyendo y calcula el diff desde su confirmación antecesora, así como git cherry-pick, pero esta vez, calcula los cambios de reversa. Eso es, si en la confirmación especificada agregaste una línea, la reversa eliminaría la línea, y vice versa.

git add -p

Poniendo en el área de preparación los cambios es una parte integral de introducir cambios a Git. A veces, deseas poner en el área de preparación todos los cambios juntos (con git add .), o tal vez poner en stage todos los cambios de un archivo específico (usando git add <file_path>). Aunque hay veces donde sería conveniente poner en stage solamente ciertas partes de los archivos modificados.

En el capítulo 6, introdujimos git add -p. Este comando te permite poner en stage ciertas partes de los archivos, al separarlos en pequeños trozos (p significa parche). Por ejemplo, digamos que tienes este archivo, my_file.py:

my_file_py_1
my_file.py

Luego modificas este archivo - al cambiar el texto dentro de function_1, y también agregando una nueva función, function_5:

 after the changes
my_file.py after the changes

Si usaste git add my_file.py a este punto, pondrías en el área de preparación ambos de estos cambios juntos. En caso de que quieras separarlos en confirmaciones distintas, podrías usar git add -p, el cual separa estos dos cambios y te pregunta sobre cada uno como un pedazo independiente:

add_p_1
git add -p

Al tipear ?, puedes ver qué significan cada opción distinta:

Using a  to get a description of the different options
Using a ? to get a description of the different options

En este caso, digamos que solamente queremos poner en el área de preparación el cambio introduciendo function_5. No queremos poner en el área de preparación el cambio de function_1, así que seleccionamos n:

Not staging the change to
Not staging the change to function_1

Luego, nos aparece una ventana para el segundo cambio - el que introduce function_5. Queremos poner en el área de preparación este pedazo, así podemos tipear y.

Resumen

Bueno, ¡esto fue divertido!

¿Puedes creer cuánto has aprendido?

En la Parte 1 aprendiste sobre blobs, árboles y confirmaciones.

Luego aprendiste sobre las ramas, viendo que no son mas que una referencia nombrada a una confirmación.

Aprendiste el proceso de registrar cambios en Git, y que involucra el directorio de trabajo, el área de preparación (índice), y el repositorio.

Luego - creaste un nuevo repositorio desde cero, al usar echo y comandos de bajo nivel tales como git hash-object. Creaste un blob, un árbol, y un objeto de confirmación apuntando a ese árbol.

En la Parte 2 aprendiste sobre hacer ramas e integrar cambios en Git.

Aprendiste lo que es un diff, y la diferencia entre un diff y un parche. También aprendiste cómo se construye la salida de git diff.

Luego, tuviste un vistazo extensivo sobre fusionar con Git, específicamente entendiendo el algoritmo de fusión de tres vías. Entendiste cuándo los conflictos de fusión ocurren, cuándo Git los puede resolver automáticamente, y cómo los resuelve de forma manual cuando sea necesario.

Viste que git rebase es poderoso - pero también es bastante sencillo una vez que entiendes lo que hace. Entendiste las diferencias entre fusionar y hacer rebase, y cuando deberías usar cada uno.

En la Parte 3 aprendiste cómo deshacer cambios en Git - especialmente cuando las cosas van mal. Aprendiste cómo usar un par de herramientas, como git reset, git commit --ammend, git revert, git reflog (y git log -g).

La herramienta más importante, aún más importante que las herramientas que listé, es limpiar la situación actual vs el que se desea. Créeme en esta, hará que cada situación parezca menos intimidante y la solución más claro.

En la Parte 4 adquiriste herramientas poderosas adicionales, como diferentes conmutadores de git log, git bisect, git cherry-pick, git revert y git add -p.

Wow, ¡deberías sentirte orgulloso!

Un mensaje de mí para tí

En efecto, eso fue divertido, pero todas las cosas deben pasar. Terminaste de leer este libro, pero no significa que tu jornada de aprendizaje termina aquí.

Lo que has adquirido, más que cualquier herramienta específica, es intuición y entendimiento de cómo opera Git, y cómo pensar sobre varias operaciones en Git. Sigue investigando, leyendo, y usando Git. Estoy seguro que serás capaz de enseñarme algo nuevo, y por todos los medios - hazlo por favor.

Si te gustó este libro, por favor compártelo con más personas.

Si quieres leer más de mis artículos de Git y manuales, aquí están:

  1. The Git Rebase Handbook
  2. The Git Merge Handbook
  3. The Git Diff and Patch Handbook
  4. Git Internals - Objects, Branches, and How to Create a Repo
  5. Git Reset Command Explained

Reconocimientos

Mucha gente ayudó en hacer este libro lo mejor que se podía. Entre ellos, fui afortunado de tener muchos lectores beta que me dieron comentarios así podía mejorar el libro. Específicamente, me gustaría agradecer a Jason S. Shapiro, Anna Łapińska, C. Bruce Hilbert, y Jonathon McKitrick por sus revisiones exhaustivas.

Abbey Rennemeyer ha sido una editor maravillosa. Después que ha revisado mis publicaciones para freeCodeCamp por tres años, fue claro que le pidiera que fuera el editor de este libro también. Ella me ayudó en mejorar el libro de muchas formas, y estoy agradecido por su ayuda.

Quincy Larson fundó la maravillosa comunidad en freeCodeCamp, me motivó por medio de emails y discusiones de cara a cara. Le agradezco por generar esta increíble comunidad, y por su amistad.

Estefania Cassingena Navone diseñó la portada de este libro. Estoy agradecido por su trabajo profesional y su paciencia con mis pedidos y perfeccionismo.

El sitio web de Daphne Gray-Grant, "Publication Coach", me dio inspiración así también como consejos técnicos que me ayudó mucho con mi proceso de escritura.

Si deseas apoyar este libro

Si te gustaría apoyar este libro, puedes comprar la versión en papel, un versión E-Book (libro electrónico), o comprarme un café. ¡Gracias!

Contáctame

Este libro ha sido creado para ayudarte y gente como tú aprenda, entienda Git, y aplique sus conocimientos en la vida real.

Bien al principio, pedí comentarios y fui afortunado de recibirlo de gente estupenda (mencionado en los Reconocimientos) para asegurar que el libro alcanzara estos objetivos. Si te gustó algo de este libro, sentiste que algo estaba faltando o necesitaba mejorar - me encantaría que me lo dijeras. Por favor háblame en: gitting.things@gmail.com.

Gracias por aprender y permitirme ser parte de tu jornada.

- Omer Rosenbaum

Apéndices

Referencias Adicionales - Por Parte

(Nota - esta es una lista corta. Puedes encontrar una lista más larga de referencias en la versión E-Book o la impresa.)

Parte 1

Parte 2

Diffs y Parches

Algoritmos de Diffs de Git:

El algoritmo de diff más predeterminado en Git es Myers:

Git Merge

Git Rebase

Recursos relacionados a los Beatles

Part 3