Capítulo 4. Control de versiones

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Mira el mundo a través de tus gafas Polaroid

Las cosas irán mucho mejor para las clases trabajadoras.

Gang of Four, "Encontré esa Esencia Rara"

Este capítulo trata de los sistemas de control de revisiones (RCS), que mantienen instantáneas de las distintas versiones de un proyecto a medida que se desarrolla, como las etapas de desarrollo de un libro, una carta de amor torturada o un programa.

Utilizar un RCS ha cambiado mi forma de trabajar. Para explicarlo con una metáfora, piensa en la escritura como en la escalada en roca. Si no eres escalador, puede que te imagines una sólida pared de roca y la intimidante y mortal tarea de llegar a la cima. Pero en la actualidad, el proceso es mucho más gradual. Atado a una cuerda, escalas unos metros, y luego enganchas la cuerda a la pared utilizando equipo especializado (levas, pasadores, mosquetones, etc.). Ahora, si te caes, tu cuerda se enganchará en el último mosquetón, lo que es razonablemente seguro. Mientras estás en la pared, tu objetivo no es llegar a la cima, sino el problema mucho más alcanzable de encontrar dónde puedes enganchar tu siguiente mosquetón.

Al volver a escribir con un RCS, un día de trabajo ya no es un camino sin características hacia la cima, sino una secuencia de pequeños pasos. ¿Qué característica podría añadir? ¿Qué problema podría solucionar? Una vez que hayas dado un paso y estés seguro de que tu base de código se encuentra en un estado seguro y limpio, confirma una revisión, y si tu siguiente paso resulta desastroso, puedes volver a la revisión que acabas de confirmar en lugar de empezar desde el principio.

Pero estructurar el proceso de escritura y permitirnos marcar puntos seguros es sólo el principio:

  • Nuestro sistema de archivos tiene ahora una dimensión temporal. Podemos consultar el repositorio de información de archivos del RCS para ver qué aspecto tenía un archivo la semana pasada y cómo ha cambiado desde entonces hasta ahora. Incluso sin los otros poderes, he descubierto que esto por sí solo me convierte en un escritor más seguro.

  • Podemos hacer un seguimiento de varias versiones de un proyecto, como mi copia y la de mi coautor. Incluso dentro de mi propio trabajo, puedo querer una versión de un proyecto (una rama) con una función experimental, que debe mantenerse separada de la versión estable que debe poder ejecutarse sin sorpresas.

  • GitHub tiene unos 314.000 proyectos que se autodeclaran principalmente en C en el momento de escribir esto, y hay más proyectos en C en otros repositorios RCS más pequeños, como Savannah de GNU. Aunque no vayas a modificar el código, clonar estos repositorios es una forma rápida de tener el programa o la biblioteca en tu disco duro para tu propio uso. Cuando tu propio proyecto esté listo para su uso público (o antes), puedes hacer público el repositorio como otro medio de distribución.

  • Ahora que tú y yo tenemos versiones del mismo proyecto, y que ambos tenemos la misma capacidad para piratear nuestras versiones del código base, el control de revisiones nos da el poder de fusionar nuestros múltiples hilos con la mayor facilidad posible.

Este capítulo tratará sobre Git, que es un sistema de control de revisiones distribuido, lo que significa que cualquier copia del proyecto funciona como un repositorio independiente del proyecto y de su historia. Existen otros, siendo Mercurial y Bazaar los otros líderes de la categoría. Existe en gran medida una correspondencia unívoca entre las características de estos sistemas, y las principales diferencias que existían se han ido fusionando a lo largo de los años, por lo que deberías ser capaz de entender los demás inmediatamente después de leer este capítulo.

Cambios mediante diff

El medio más rudimentario de control de revisiones es a través de diff y patch, que son estándar POSIX y, por tanto, seguramente están en tu sistema. Probablemente tengas dos archivos en algún lugar de tu disco duro que sean razonablemente similares; si no es así, coge cualquier archivo de texto, cambia unas cuantas líneas y guarda la versión modificada con un nuevo nombre. Inténtalo:

diff f1.c f2.c

y obtendrás un listado, un poco más legible por la máquina que por el ser humano, que muestra las líneas que han cambiado entre los dos archivos. Canalizar la salida a un archivo de texto a través de diff f1.c f2.c >diffs y luego abrir diffs en tu editor de texto puede darte una versión coloreada más fácil de seguir. Verás algunas líneas que indican el nombre del archivo y su ubicación dentro del mismo, quizás algunas líneas de contexto que no cambiaron entre los dos archivos, y líneas que empiezan por + y - que muestran las líneas que se añadieron y eliminaron. Ejecuta diff con la bandera -u para obtener unas cuantas líneas de contexto alrededor de las adiciones y sustracciones.

Dados dos directorios que contienen dos versiones de tu proyecto v1 y v2genera un único archivo diff en el formato diff unificado para todos los directorios mediante la opción recursiva (-r):

diff -ur v1 v2 > diff-v1v2

El comando patch lee los archivos diff y ejecuta los cambios que allí se indican. Si tú y un amigo tenéis v1 del proyecto, podrías enviar diff-v1v2 a tu amiga, y ella podría ejecutar

patch < diff-v1v2

para aplicar todos tus cambios a su copia de v1.

O, si no tienes amigos, puedes ejecutar diff de vez en cuando en tu propio código y mantener así un registro de los cambios que has hecho a lo largo del tiempo. Si descubres que has introducido un error en tu código, los diffs son el primer lugar donde buscar pistas sobre lo que has tocado que no deberías haber tocado. Si eso no es suficiente, y ya has borrado v1puedes ejecutar el parche a la inversa desde el directorio v2patch -R <diff-v1v2revirtiendo la versión 2 a la versión 1. Si estuvieras en la versión 4, podrías incluso ejecutar una secuencia de diffs para retroceder más en el tiempo:

cd v4
patch -R < diff-v3v4
patch -R < diff-v2v3
patch -R < diff-v1v2

Digo posiblemente porque mantener una secuencia de diffs como ésta es tedioso y propenso a errores. De ahí el sistema de control de revisiones, que hará y seguirá los diffs por ti.

Objetos de Git

Git es un programa en C como cualquier otro, y se basa en un pequeño conjunto de objetos. El objeto clave es el objeto de confirmación, que es similar a un archivo diff unificado. Dado un objeto de confirmación anterior y algunos cambios desde esa línea de base, un nuevo objeto de confirmación encapsula la información. Obtiene cierto apoyo del índice, que es una lista de los cambios registrados desde el último objeto de confirmación, cuyo uso principal será generar el siguiente objeto de confirmación.

Los objetos de confirmación se enlazan entre sí para formar un árbol como cualquier otro árbol. Cada objeto de confirmación tendrá (al menos) un objeto de confirmación padre. Subir y bajar por el árbol es como utilizar patch y patch -R para pasar de una versión a otra.

El repositorio en sí no es formalmente un objeto único en el código fuente de Git, pero yo pienso en él como un objeto, porque las operaciones habituales que uno definiría, como nuevo, copiar y liberar, se aplican a todo el repositorio. Obtén un nuevo repositorio en el directorio en el que estás trabajando mediante:

git init

Bien, ya tienes un sistema de control de revisiones. Puede que no lo veas, porque Git almacena todos sus archivos en un directorio llamado .git, donde el punto significa que todas las utilidades habituales como ls lo tomarán como oculto. Puedes buscarlo a través de, por ejemplo, ls -a o mediante la opción mostrar archivos ocultos de tu gestor de archivos favorito.

Alternativamente, copia un repositorio a través de git clone. Así es como obtendrías un proyecto de Savannah o Github. Para obtener el código fuente de Git utilizando git:

git clone https://github.com/gitster/git.git

Al lector también le puede interesar clonar el repositorio con los ejemplos de este libro:

git clone https://github.com/b-k/21st-Century-Examples.git

Si quieres probar algo en un repositorio en ~/myrepo y te preocupa que puedas romper algo, ve a un directorio temporal (por ejemplo mkdir ~/tmp; cd ~/tmp), clona tu repositorio con git clone ~/myrepoy experimenta. Borrar el clon cuando hayas terminado (rm -rf ~/tmp/myrepo) no afecta al original.

Dado que todos los datos sobre un repositorio están en el subdirectorio .git del directorio de tu proyecto, la analogía para liberar un repositorio es sencilla:

rm -rf .git

Tener todo el repositorio tan autocontenido significa que puedes hacer copias de repuesto para intercambiarlas entre casa y el trabajo, copiarlo todo a un directorio temporal para un experimento rápido, etc., sin mucha complicación.

Estamos casi listos para generar algunos objetos de confirmación, pero como resumen las diferencias desde el punto de partida o desde una confirmación anterior, vamos a tener que tener a mano algunas diferencias para confirmar. El índice (fuente Git: struct index_state) es una lista de cambios que se van a incluir en la siguiente confirmación. Existe porque en realidad no queremos que se registren todos los cambios del directorio del proyecto. Por ejemplo gnomes.c y gnomes.h engendrarán gnomes.o y el ejecutable gnomes. Tu RCS debe rastrear gnomes.c y gnomes.h y dejar que los demás se regeneren según sea necesario. Así que la operación clave con el índice es añadir elementos a su lista de cambios. Utilízalo:

git add gnomes.c gnomes.h

para añadir estos archivos al índice. Otros cambios típicos en la lista de archivos rastreados también deben registrarse en el índice:

git add newfile
git rm oldfile
git mv flie file

Los cambios que realices en archivos que ya están rastreados por Git no se añaden automáticamente al índice, lo que puede ser una sorpresa para los usuarios de otros RCS (pero mira más abajo). Añade cada uno individualmente a través de git add changedfileo utiliza:

git add -u

para añadir al índice los cambios de todos los archivos que Git ya rastrea.

En algún momento tendrás suficientes cambios listados en el índice como para que se registren como un objeto de confirmación en el repositorio. Genera un nuevo objeto de confirmación mediante:

git commit -a -m "here is an initial commit."

La bandera -m adjunta un mensaje a la revisión, que leerás cuando ejecutes git log más adelante. Si omites el mensaje, Git iniciará el editor de texto especificado en la variable de entorno EDITOR para que puedas introducirlo (el editor por defecto suele ser vi; exporta esa variable en el script de inicio de tu shell, por ejemplo, .bashrc o .zshrc, si quieres algo diferente).

La bandera -a le dice a Git que hay muchas probabilidades de que haya olvidado ejecutar git add -u, así que, por favor, ejecútalo justo antes de confirmar. En la práctica, esto significa que nunca tendrás que ejecutar git add -u explícitamente, siempre que recuerdes la bandera -a en git commit -a.

Advertencia

Es fácil encontrar expertos en Git que se preocupan por generar una narrativa coherente y limpia a partir de sus commits. En lugar de mensajes de commit como "añadido un objeto índice, más algunas correcciones de errores por el camino", un autor experto en Git crearía dos commits, uno con el mensaje "añadido un objeto índice" y otro con "correcciones de errores". Estos autores tienen tal control porque no se añade nada al índice por defecto, así que pueden añadir sólo lo suficiente para expresar un cambio preciso en el código, escribir el índice en un objeto de confirmación, y luego añadir un nuevo conjunto de elementos a un índice limpio para generar el siguiente objeto de confirmación.

Encontré un bloguero que dedicó varias páginas a describir su rutina de commit: "Para los casos más complicados, imprimiré los diffs, los leeré por encima y los marcaré con seis colores de rotulador fluorescente...". Sin embargo, hasta que no te conviertas en un experto en Git, esto supondrá mucho más control sobre el índice del que realmente necesitas o quieres. Es decir, no utilizar -a con git commit es un uso avanzado con el que mucha gente nunca se molesta. En un mundo perfecto, el -a sería el predeterminado, pero no lo es, así que no lo olvides.

Al llamar a git commit -a se escribe un nuevo objeto de confirmación en el repositorio basado en todos los cambios que el índice ha podido seguir, y se borra el índice. Habiendo guardado tu trabajo, ahora puedes seguir añadiendo más. Además -y ésta es la principal ventaja real del control de revisiones hasta ahora- puedes borrar lo que quieras, con la seguridad de que podrás recuperarlo si lo necesitas. No abarrotes el código con grandes bloques de rutinas obsoletas comentadas: ¡elimínalas!

Nota

Después de confirmar, es casi seguro que te des una palmada en la frente y te des cuenta de algo que has olvidado. En lugar de realizar otro commit , puedes ejecutar git commit --amend -a para rehacer tu último commit.

Una vez generado un objeto de confirmación, tus interacciones con él consistirán principalmente en mirar su contenido. Utilizarás git diff para ver los diffs que son el núcleo del objeto commit y git log para ver los metadatos.

Los metadatos clave son el nombre del objeto, que se asigna mediante una convención de nomenclatura desagradable pero sensata: el hash SHA1, un número hexadecimal de 40 dígitos que puede asignarse a un objeto, de forma que nos permita suponer que no habrá dos objetos con el mismo hash, y que el mismo objeto tendrá el mismo nombre en cada copia del repositorio. Cuando confirmes tus archivos, verás los primeros dígitos del hash en la pantalla, y puedes ejecutar git log para ver la lista de objetos confirmados en el historial del objeto de confirmación actual, listados por su hash y por el mensaje en lenguaje humano que escribiste cuando hiciste la confirmación (y consulta git help log para ver el resto de metadatos disponibles). Afortunadamente, sólo necesitas la parte del hash que identifique de forma única tu confirmación. Así que si miras el registro y decides que quieres comprobar el número de revisión fe9c49cddac5150dc974de1f7248a1c5e3b33e89, puedes hacerlo con:

git checkout fe9c4

Esto hace el tipo de viaje en el tiempo a través de diffs que patch casi proporciona, rebobinando al estado del proyecto en el commit fe9c4.

Como una confirmación dada sólo tiene punteros a sus padres, no a sus hijos, cuando consultes git log después de consultar una confirmación antigua, verás el rastro de los objetos que llevaron a esta confirmación, pero no las posteriores. El raramente utilizado git reflog te mostrará la lista completa de objetos de confirmación que conoce el repositorio, pero la forma más fácil de volver a la versión más actual del proyecto es a través de una etiqueta, un nombre amigable que no tendrás que buscar en el registro. Las etiquetas se mantienen como objetos separados en el repositorio y contienen un puntero a un objeto de confirmación que se está etiquetando. La etiqueta que se utiliza con más frecuencia es master, que se refiere al último objeto de confirmación de la rama maestra (que, como aún no hemos tratado el tema de las ramas, probablemente sea la única rama que tengas). Así, para volver atrás en el tiempo hasta el último estado, utiliza

git checkout master

Volviendo a git diff, muestra los cambios que has hecho desde la última revisión confirmada. La salida es lo que se escribiría en el siguiente objeto de confirmación mediante git commit -a. Al igual que con la salida del programa normal diff, git diff > diffs escribirá en un archivo que puede ser más legible en tu editor de texto coloreado.

Sin argumentos, git diff muestra la diferencia entre el índice y lo que hay en el directorio del proyecto; si aún no has añadido nada al índice, serán todos los cambios desde la última confirmación. Con un nombre de objeto de confirmación, git diff muestra la secuencia de cambios entre esa confirmación y lo que hay en el directorio del proyecto. Con dos nombres, muestra la secuencia de cambios de una confirmación a otra:

git diff                Show the diffs between the working directory and the index.
git diff --staged       Show the diffs between the index and the previous commit.
git diff 234e2a         Show the diffs between the working directory and the given commit object.
git diff 234e2a 8b90ac  Show the changes from one commit object to another.
Nota

Hay algunas conveniencias de nomenclatura para ahorrarte algo de hexadecimal. El nombre HEAD se refiere a la última confirmación desprendida. Suele ser la punta de una rama; cuando no lo es, los mensajes de error de git se refieren a ella como "desprendida HEAD."

Añade ~1 a un nombre para referirte al padre de la confirmación nombrada, ~2 para referirte a su abuelo, y así sucesivamente. Así, todos los siguientes son válidos:

git diff HEAD~4         #Compare the working directory to four commits ago.
git checkout master~1   #Check out the predecessor to the head of the master branch.
git checkout master~    #Shorthand for the same.
git diff b0897~ b8097   #See what changed in commit b8097.

Llegados a este punto, ya sabes cómo hacerlo:

  • Guarda revisiones incrementales frecuentes de tu proyecto.

  • Obtén un registro de tus revisiones comprometidas.

  • Averigua qué has cambiado o añadido recientemente.

  • Comprueba las versiones anteriores para poder recuperar el trabajo anterior si es necesario.

Tener un sistema de copias de seguridad lo suficientemente organizado como para que puedas borrar código con confianza y recuperarlo cuando sea necesario ya te convertirá en un mejor escritor.

El alijo

Los objetos de confirmación son los puntos de referencia desde los que se produce la mayor parte de la actividad de Git. Por ejemplo, Git prefiere aplicar parches relativos a un commit, y puedes saltar a cualquier commit, pero si saltas de un directorio de trabajo que no coincide con un commit no tienes forma de volver a saltar. Cuando hay cambios no comprometidos en el directorio de trabajo actual, Git te advertirá de que no estás en una confirmación y normalmente se negará a realizar la operación que le pediste. Una forma de volver a una confirmación sería anotar todo el trabajo que has hecho desde la última confirmación, revertir tu proyecto a la última confirmación, ejecutar la operación, y luego rehacer el trabajo guardado cuando hayas terminado de saltar o parchear.

Así empleamos el stash, un objeto especial de confirmación equivalente en su mayor parte al que obtendrías de git commit -a, pero con algunas características especiales, como conservar toda la basura sin rastrear en tu directorio de trabajo. Éste es el procedimiento típico:

git stash
# Code is now as it was at last checkin.
git checkout fe9c4

# Look around here.

git checkout master    # Or whatever commit you had started with
# Code is now as it was at last checkin, so replay stashed diffs with:
git stash pop

Otra alternativa a veces apropiada para comprobar los cambios realizados en tu directorio de trabajo es git reset --hard, que devuelve el directorio de trabajo al estado en que se encontraba la última vez que lo comprobaste. El comando suena severo porque lo es: estás a punto de tirar por la borda todo el trabajo que has hecho desde la última comprobación.

Los árboles y sus ramas

Hay un árbol en un repositorio, que se generó cuando el primer autor de un nuevo repositorio ejecutó git init. Probablemente estés familiarizado con las estructuras de datos en árbol, que consisten en un conjunto de nodos, donde cada nodo tiene enlaces a cierto número de hijos y un enlace a un padre (y en árboles exóticos como el de Git, posiblemente varios padres).

De hecho, todos los objetos de confirmación excepto el inicial tienen un padre, y el objeto registra las diferencias entre él y la confirmación padre. El nodo terminal de la secuencia, la punta de la rama, se etiqueta con un nombre de rama. A nuestros efectos, existe una correspondencia unívoca entre las puntas de las ramas y la serie de cambios que condujeron a esa rama. La correspondencia uno a uno significa que podemos referirnos indistintamente a las ramas y al objeto de confirmación en la punta de la rama. Así, si la punta de la rama master es el commit 234a3d, entonces git checkout master y git checkout 234a3d son totalmente equivalentes (hasta que se escriba un nuevo commit, y éste adopte la etiqueta master ). También significa que la lista de objetos de confirmación de una rama puede recuperarse en cualquier momento empezando por la confirmación en la punta nombrada y remontándose hasta el origen del árbol.

La costumbre típica es mantener la rama maestra totalmente funcional en todo momento. Cuando quieras añadir una nueva función o probar un nuevo hilo de investigación, crea una nueva rama para ello. Cuando la rama esté en pleno funcionamiento, podrás fusionar la nueva función de nuevo en la maestra utilizando los métodos que se indican a continuación.

Hay dos formas de crear una nueva rama partiendo del estado actual de tu proyecto:

git branch newleaf       # Create a new branch...
git checkout newleaf     # then check out the branch you just created.
    # Or execute both steps at once with the equivalent:
git checkout -b newleaf

Una vez creada la nueva rama, cambia entre las puntas de las dos ramas mediante git checkout master y git checkout newleaf.

¿En qué rama estás ahora mismo? Averígualo con:

git branch

que listará todas las ramas y pondrá un * junto a la que esté activa en ese momento.

¿Qué pasaría si construyeras una máquina del tiempo, volvieras a antes de nacer y mataras a tus padres? Si algo hemos aprendido de la ciencia ficción, es que si cambiamos la historia, el presente no cambia, sino que se escinde una nueva historia alternativa. Así que si revisas una versión antigua, haces cambios, y revisas un nuevo objeto de confirmación con tus cambios recién hechos, entonces ahora tienes una nueva rama distinta de la rama maestra. Encontrarás a través de git branch que cuando el pasado se bifurque así, estarás en (no branch). Las ramas no etiquetadas tienden a crear problemas, así que si alguna vez descubres que estás haciendo trabajo en (no branch), entonces ejecuta git branch -m new_branch_name para nombrar la rama a la que te acabas de bifurcar.

Fusión

Hasta ahora, hemos generado nuevos objetos de confirmación partiendo de un objeto de confirmación como punto de partida y aplicando una lista de diferencias del índice. Una rama también es una serie de diffs, así que dado un objeto de confirmación arbitrario y una lista de diffs de una rama, deberíamos ser capaces de crear un nuevo objeto de confirmación en el que los diffs de la rama se apliquen al objeto de confirmación existente. Esto es una fusión. Para fusionar todos los cambios ocurridos en el transcurso de newleaf en master, cambia a master y utiliza git merge:

git checkout master
git merge newleaf

Por ejemplo, has utilizado una rama de master para desarrollar una nueva función, y finalmente pasa todas las pruebas; entonces, aplicar todos los diffs de la rama de desarrollo a master crearía un nuevo objeto commit con la nueva función sólidamente instalada.

Digamos que, mientras trabajabas en la nueva función, nunca comprobaste master y, por tanto, no hiciste ningún cambio en ella. Entonces, aplicar la secuencia de diffs de la otra rama sería simplemente una repetición rápida de todos los cambios registrados en cada objeto commit de la rama, lo que Git denomina un avance rápido.

Pero si has hecho algún cambio en master, entonces ya no se trata simplemente de una aplicación rápida de todos los diffs. Por ejemplo, digamos que en el punto en que se separó la rama, gnomes.c tenía:

short int height_inches;

En master, has eliminado el tipo despectivo:

int height_inches;

El propósito de newleaf era convertir al sistema métrico decimal:

short int height_cm;

En este punto, Git está bloqueado. Saber cómo combinar estas líneas requiere saber qué pretendías tú como humano. La solución de Git es modificar tu archivo de texto para incluir ambas versiones, algo así como

<<<<<<< HEAD
int height_inches;
=======
short int height_cm;
>>>>>>> 3c3c3c

La fusión queda en suspenso, a la espera de que edites el archivo para expresar el cambio que te gustaría ver. En este caso, probablemente reducirías el trozo de cinco líneas que Git dejó en el archivo de texto a:

int height_cm;

Éste es el procedimiento para confirmar fusiones en un avance no rápido, lo que significa que ha habido cambios en ambas ramas desde que divergieron:

  1. Ejecuta git merge other_branch.

  2. Lo más probable es que te digan que hay conflictos que tienes que resolver.

  3. Comprueba la lista de archivos no fusionados utilizando git status.

  4. Elige un archivo para comprobarlo manualmente. Ábrelo en un editor de texto y busca las marcas merge-me si se trata de un conflicto de contenido. Si es un conflicto de nombre o posición de archivo, muévelo a su sitio.

  5. Ejecuta git add your_now_fixed_file.

  6. Repite los pasos 3-5 hasta que todos los archivos no fusionados estén registrados.

  7. Ejecuta git commit para finalizar la fusión.

Consuélate con todo este trabajo manual. Git es conservador a la hora de fusionar y no hará automáticamente nada que pueda, bajo algún argumento, hacerte perder trabajo.

Cuando termines la fusión, todas las diferencias relevantes que se hayan producido en la rama lateral estarán representadas en el objeto de confirmación final de la rama fusionada, por lo que lo habitual es eliminar la rama lateral:

git branch -d other_branch

La etiqueta other_branch se borra, pero los objetos de confirmación que condujeron a ella siguen en el repositorio para tu referencia.

La Rebase

Supongamos que tienes una rama principal y que el lunes separas de ella una rama de pruebas. Luego, de martes a jueves, haces grandes cambios tanto en la rama principal como en la de pruebas. El viernes, cuando intentas fusionar de nuevo la rama de pruebas con la principal, tienes un número abrumador de pequeños conflictos que resolver.

Volvamos a empezar la semana. El lunes separaste la rama de pruebas de la rama principal, lo que significa que las últimas confirmaciones en ambas ramas comparten un ancestro común de la confirmación del lunes en la rama principal. El martes, tienes un nuevo commit en la rama principal; que sea el commit abcd123. Al final del día, reproduces en la rama de pruebas todas las diferencias que se produjeron en la rama principal:

git branch testing  # get on the testing branch
git rebase abcd123  # or equivalently: git rebase main

Con el comando rebase, todos los cambios realizados en la rama principal desde el ancestro común se reproducen en la rama de pruebas. Puede que tengas que fusionar cosas manualmente, pero al tener sólo un día de trabajo para fusionar, podemos esperar que la tarea de fusionar sea más manejable.

Ahora que todos los cambios hasta abcd123 están presentes en ambas ramas, es como si las ramas se hubieran separado realmente de esa confirmación, en lugar de la confirmación del lunes. De ahí viene el nombre del procedimiento: la rama de pruebas se ha rebasado para separarse de un nuevo punto de la rama principal.

También realizas rebases al final del miércoles, jueves y viernes, y cada uno de ellos es razonablemente indoloro, ya que la rama de pruebas se mantuvo al día con los cambios en la rama principal durante toda la semana.

A menudo se considera que rebasar es un uso avanzado de Git, porque otros sistemas que no son tan capaces de aplicar diffs no disponen de esta técnica. Pero en la práctica, el rebase y la fusión son prácticamente iguales: ambos aplican los diffs de otra rama para producir un commit, y la única cuestión es si estás uniendo los extremos de dos ramas (en cuyo caso, fusión) o quieres que ambas ramas continúen sus vidas separadas durante un tiempo más (en cuyo caso, rebase). El uso típico es rebasear los diffs de la rama maestra a la rama lateral, y fusionar los diffs de la rama lateral a la maestra, por lo que en la práctica hay una simetría entre ambas. Y, como ya se ha dicho, dejar que las diferencias se acumulen en varias ramas puede hacer que la fusión final sea un engorro, por lo que es recomendable hacer rebase con una frecuencia razonable.

Repositorios remotos

Hasta este punto, todo ha ocurrido dentro de un mismo árbol. Si has clonado un repositorio de otro lugar, en el momento de la clonación, tanto tú como el origen tenéis árboles idénticos con objetos de confirmación idénticos. Sin embargo, tú y tus compañeros seguiréis trabajando, por lo que todos añadiréis nuevos y diferentes objetos de confirmación.

Tu repositorio tiene una lista de remotos, que son punteros a otros repositorios relacionados con éste en otras partes del mundo. Si obtuviste tu repositorio a través de git clone, entonces el repositorio desde el que clonaste se llama origin en lo que respecta al nuevo repositorio. En el caso típico, éste es el único remoto que utilizarás.

Cuando clones por primera vez y ejecutes git branch, verás una única rama solitaria, independientemente de cuántas ramas tuviera el repositorio de origen. Pero ejecuta git branch -a para ver todas las ramas que Git conoce, y verás tanto las remotas como las locales. Si clonaste un repositorio de Github, et al, puedes utilizar esto para comprobar si otros autores habían empujado otras ramas al repositorio central.

Esas copias de las ramas en tu repositorio local son las de la primera vez que tiraste. La semana que viene, para actualizar esas ramas remotas con la información del repositorio de origen, ejecuta git fetch.

Ahora que tienes copias actualizadas de las ramas remotas en tu repositorio, podrías fusionar una con la rama local en la que estás trabajando utilizando el nombre completo de la rama remota, por ejemplo, git merge remotes/origin/master.

En lugar de los dos pasos de git fetch; git merge remotes/origin/master, puedes actualizar la rama mediante

git pull origin master

que obtiene los cambios remotos y los fusiona en tu repositorio actual de una sola vez.

Lo contrario es push, que utilizarás para actualizar el repositorio remoto con tu última confirmación (no con el estado de tu índice o directorio de trabajo). Si estás trabajando en una rama llamada bbranch y quieres hacer un push al remoto con el mismo nombre, utiliza:

git push origin bbranch

Hay muchas probabilidades de que cuando envíes tus cambios, aplicar los diffs de tu rama a la rama remota no sea un avance rápido (si lo es, es que tus colegas no han estado trabajando). Resolver una fusión no rápida suele requerir la intervención humana, y probablemente no haya ningún humano en la rama remota. Por lo tanto, Git sólo permite los push de avance rápido. ¿Cómo puedes garantizar que tu push es un avance rápido?

  1. Ejecuta git pull origin bbranch para obtener los cambios realizados desde tu última extracción.

  2. Fusionar, como se ha visto antes, en la que tú, como humano, resuelves los cambios que un ordenador no puede.

  3. Ejecuta git commit -a -m "dealt with merges".

  4. Ejecuta git push origin bbranchporque ahora Git sólo tiene que aplicar un único diff, lo que puede hacerse automáticamente.

Hasta ahora, he supuesto que estás en una rama local con el mismo nombre que la rama remota (probablemente master en ambos lados). Si estás cruzando nombres, da un par de caracteres separados por dos puntos source:destination nombres de rama.

git fetch origin new_changes:master #Merge remote new_changes into local master
git push origin my_fixes:version2  #Merge the local branch into a differently named remote.
git push origin :prune_me          #Delete a remote branch.
git fetch origin new_changes:      #Pull to no branch; create a commit named FETCH_HEAD.

Ninguna de estas operaciones cambia tu rama actual, pero algunas crean una nueva rama a la que puedes cambiar mediante el método habitual git checkout.

La estructura de un repositorio Git no es especialmente compleja: hay objetos de confirmación que representan los cambios desde el objeto de confirmación padre, organizados en un árbol, con un índice que reúne los cambios que se harán en la siguiente confirmación. Pero con estos elementos, puedes organizar múltiples versiones de tu trabajo, eliminar cosas con confianza, crear ramas experimentales y fusionarlas de nuevo con el hilo principal cuando superen todas sus pruebas, y fusionar el trabajo de tus colegas con el tuyo propio. A partir de ahí, git help y tu buscador de Internet favorito te enseñarán muchos más trucos y formas de hacer estas cosas con más soltura.

Get C del siglo XXI, 2ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.