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:
difff1.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 v2
genera un único archivo diff en el formato diff unificado para todos los directorios mediante la opción recursiva (-r
):
diff -urv1
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 v1
puedes ejecutar el parche a la inversa desde el directorio v2
patch -R <
diff-v1v2
revirtiendo 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:
cdv4
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
~/myrepo
y 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 addgnomes.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 addnewfile
git rmoldfile
git mvflie 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
changedfile
o 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 diff234e2a
Show the diffs between the working directory and the given commit object. git diff234e2a 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 checkoutfe9c4
# Look around here. git checkoutmaster
# 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 branchnewleaf
# Create a new branch... git checkoutnewleaf
# then check out the branch you just created. # Or execute both steps at once with the equivalent: git checkout -bnewleaf
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
;
>>>>>>>
3
c3c3c
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:
-
Ejecuta
git merge
other_branch
. -
Lo más probable es que te digan que hay conflictos que tienes que resolver.
-
Comprueba la lista de archivos no fusionados utilizando
git status
. -
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.
-
Ejecuta
git add
your_now_fixed_file
. -
Repite los pasos 3-5 hasta que todos los archivos no fusionados estén registrados.
-
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?
-
Ejecuta
git pull origin
bbranch
para obtener los cambios realizados desde tu última extracción. -
Fusionar, como se ha visto antes, en la que tú, como humano, resuelves los cambios que un ordenador no puede.
-
Ejecuta
git commit -a -m "
dealt with merges
"
. -
Ejecuta
git push origin
bbranch
porque 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 remotenew_changes
into localmaster
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 namedFETCH_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.