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

 

 


Tema destacado: Security Series.XSS. [Cross Site Scripting]


+  Foro de elhacker.net
|-+  Programación
| |-+  Programación General
| | |-+  Trabajando con las ramas de git (tercera parte)
0 Usuarios y 1 Visitante están viendo este tema.
Páginas: [1] Ir Abajo Respuesta Imprimir
Autor Tema: Trabajando con las ramas de git (tercera parte)  (Leído 1,070 veces)
MinusFour
Moderador Global
***
Desconectado Desconectado

Mensajes: 5.119


I'm fourth.


Ver Perfil WWW
Trabajando con las ramas de git (tercera parte)
« en: 14 Diciembre 2020, 18:30 pm »

Indice

Prefacio

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

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

Las ramas en git

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

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

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

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

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

Listando las ramas

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



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

Código
  1. rm -f password.txt

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

Código
  1. git branch

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



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



Creando una nueva rama

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

Para crear una rama simplemente usare:

Código
  1. git branch ramanueva



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

Cambiando a la nueva rama

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

Código
  1. git checkout rama/commit

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



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



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



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

Trabajando sobre la nueva rama

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



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

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


Cuando hicimos git checkout sobre la nueva rama:


Y cuando agregamos los tres commits:


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

Código
  1. git checkout master

Y si queremos volver a nuestros cambios:

Código
  1. git checkout cambios-importantes

Usando multiples ramas para mantener multiples versiones

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



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

Código
  1. git checkout -b ramanueva



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



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



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

Código
  1. git log --graph

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



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


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

Código
  1. git checkout -b nuevarama puntodepartida



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



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



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



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


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

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

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

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

Lo probaremos sobre la rama cambios-importantes:



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

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



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

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



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

Eliminando una rama

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

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



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



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



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

Código
  1. git branch rama commit

Y no perderíamos absolutamente nada.

Integrando cambios de una rama a otra

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

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


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


Esto es un trabajo para git merge.

Código
  1. git merge rama

Veamos que es lo que hace:



Fast Foward Merge

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

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



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


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

3-Way Merge

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



Y usando la gráfica:


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

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



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



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



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



En la gráfica:


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

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

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



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


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


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

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

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

Deshaciendo un merge

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

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



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

Resolviendo conflictos

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



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



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



Ahora podemos hacer nuestro merge nuevamente:



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



Para visualizar mejor las diferencias usare la tabla anterior:


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



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



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

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



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



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



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



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

Opciones para git merge

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

Primero, tenemos el argumento --ff-only.

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

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

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

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

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

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

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

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

Lo caótico que pueden ser los merge commits

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


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



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



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




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



Revisemos nuevamente el estado de nuestro historial:



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


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




Alterando nuestras ramas para tener un historial más simple

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

Código
  1. git rebase rama/commit

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



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


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

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



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

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

Código
  1. git rebase --continue

Deshaciendo un rebase

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



Incluyendo cambios sin hacer un merge propio

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

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

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

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




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



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


Caso más elaborado para git rebase

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


Y aplicarlos sobre 0a4e950.

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



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



Resolviendo un conflicto de git rebase

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



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

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

Código
  1. git rebase --skip

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



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



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

Como funciona git rebase realmente

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

Código
  1. git rebase A

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

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

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

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

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

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

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

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


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

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

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

Epílogo

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

Por último, mencionare que hay una tendencia por parte de los usuarios de git a favorecer git rebase sobre git merge y viceversa. Son dos herramientas diferentes que por lo general son utilizadas en conjunto y no se excluyen mutuamente. "git rebase es mejor" o "git merge es mejor" son opiniones erroneas, cada una tiene su uso y es necesario entender cuando sería mejor usar una y cuando usar las otra.


« Última modificación: 7 Junio 2021, 17:01 pm por MinusFour » En línea

Páginas: [1] Ir Arriba Respuesta Imprimir 

Ir a:  

Mensajes similares
Asunto Iniciado por Respuestas Vistas Último mensaje
codigo fuente de que ladilla tercera parte
Programación Visual Basic
*kran 0 2,387 Último mensaje 28 Junio 2007, 01:19 am
por *kran
China produce y aloja una tercera parte del malware mundial
Noticias
wolfbcn 0 1,091 Último mensaje 21 Junio 2011, 15:22 pm
por wolfbcn
Más de la tercera parte de las personas que se cambian al Galaxy S5 de ....
Noticias
wolfbcn 0 650 Último mensaje 14 Abril 2014, 21:43 pm
por wolfbcn
Ramas del hacking
Foro Libre
Baal_30 0 1,165 Último mensaje 11 Octubre 2015, 15:31 pm
por Baal_30
Para los 20 años del foro existe una tercera parte de la historia?
Sugerencias y dudas sobre el Foro
rdzlcs 8 5,074 Último mensaje 23 Agosto 2020, 17:44 pm
por SeNeGe
WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines