Ramificaciones en Git

Branching, Merging, Colaboración y Rebase

¿Que es una rama/branch?

Para entender realmente como ramifica Git, tenemos que examinar la forma en que almacena sus datos.

Recordemos que en cada confirmacion de cambios, Git almacena una instantanea (snapshot). Dicha snapshot contiene ademas unos metadatos con el autor, mensaje explicativo, y una o varias referencias a las confirmaciones que sean padres directos de esta.


Un padre en los casos de confirmacion normal, y multiples padres en los casos de estar confirmando una fusion merge de dos o mas ramas.


Supongamos que tenemos una carpeta con 3 archivos, preparamos (stage) todos ellos y confirmamos. Al preparar los archivos, Git realiza un checksum para cada uno, almacena una copia de cada uno en el repositorio (las copias se denominan blobs), y guarda cada checksum en el area de preparacion (staging):

git add README test.rb LICENSE
git commit -m 'initial commit of my project'

Cuando creas una confirmación con el comando git commit, Git realiza checksums de cada subdirectorio, y las guarda como objetos arbol (tree) en el repositorio Git. Despues, Git crea un objeto de confirmación con los metadatos pertinentes y una referencia al objeto arbol raiz del proyecto.


En este momento, el repositorio de Git contendra cinco objetos:

  • un blob para cada uno de los 3 archivos.
  • un tree con la lista de contenidos del directorio.
  • un commit referenciando a la raiz de ese tree y conteniendo el resto de metadatos pertinentes.
Diagrama de arbol de git

Si hacemos mas cambios y volvemos a confirmar, la siguiente confirmacion guardara una referencia a su confirmacion precedente.

Diagrama de arbol de git

Una rama de Git es simplemente una referencia movil apuntando a una de estas confirmaciones (commit). La rama por defecto de Git es la rama master. Con la primera confirmacion de cambios que realicemos, se creara esta rama principal master apuntando a dicha confirmacion. En cada confirmacion de cambios que realicemos, la rama ira avanzando automaticamente.

Diagrama de arbol de git

Crear una Nueva Rama

¿Que sucede cuando creamos una nueva rama?. Simplemente se crea una nueva referencia para que la puedas mover libremente. Por ejemplo supongamos que creamos una nueva rama testing. Para eso usamos el comando git branch:

git branch testing
Diagrama de ramas de git (2 ramas)

¿Como sabe Git en que rama estamos en este momento?. Lo hace mediante una referencia/puntero especial denominado HEAD. Este apunta a la rama local en la que estemos en este momento, en este caso la rama master, ya que el comando git branch solamente crea una nueva rama, pero no salta a dicha rama.

Esto se puede ver facil con git log --decorate:

git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project
Diagrama de ramas de git (HEAD ~ master)

Cambiar de Rama

Para saltar de una rama a otra, tienes que utilizar el comando git checkout. El siguiente comando mueve la referencia HEAD a la rama testing:

git checkout testing
Diagrama de ramas de git (HEAD ~ testing)

Despues del commit

¿Cual es el significado de todo esto?. Lo veremos tras realizar otra confirmacion:

git commit -am 'small changes'

Observamos algo interesante: la rama testing avanza, mientras que la rama master permanece en la confirmacion donde estaba cuando lanzamos el comando git checkout para saltar.

Diagrama de ramas de git (HEAD ~ testing) despues del commit

Ahora volvamos a la rama master:

git checkout master

Este comando realiza 2 acciones:

  • Mueve la referencia HEAD de nuevo a la rama master.
  • Revierte los archivos de tu directorio de trabajo, dejandolos tal y como estaban en la ultima snapshot de la rama master.
Diagrama de ramas de git (HEAD ~ master) despues del commit

Bifurcacion

Hagamos algunos cambios mas y confirmemoslos:

git commit -am 'thanks for the opportunity'

Ahora el historial del proyecto diverge. Los cambios realizados en ambas sesiones de trabajo (master y testing) estan aislados en ramas independientes. Podemos saltar libremente entre una y otra segun estimemos oportuno. Y todo con 3 simples comandos: git branch, git checkout y git commit

Diagrama de ramas de git (HEAD ~ master) bifurcacion

Tambien podemos ver la bifurcacion facilmente utilizando el comando git log. Ejecutaremos el siguiente comando para ver el historial de las confirmaciones, indicando donde estan las referencias a las ramas y como ha divergido el historial.

git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

Debido a que una rama de Git es realmente un simple archivo que contiene 40 caracteres de un checksum SHA-1, no cuesta nada crear y destruir ramas en Git. Crear una nueva rama es tan rapido y simple como escribir 41 bytes en un archivo.


Procedimientos Basicos

Ramificar y Fusionar

Contexto

Vamos a presentar un ejemplo simple de ramificar y fusionar. Imaginemos que seguimos los siguientes pasos:

  • Trabajas en un sitio web.
  • Creas una rama para un nuevo tema sobre el que quieres trabajar.
  • Realizas algo de trabajo en esa rama.

En este momento, recibes una llamada avisandote de un problema critico que debes resolver. Y sigues los siguientes pasos:

  • Vuelves a la rama de produccion original.
  • Creas una nueva rama para el problema critico y lo resuelves trabajando en ella.
  • Tras las pertinentes pruebas, fusionas (merge) esa rama y la envias (push) a la rama de produccion.
  • Vuelves a la rama del tema en que andabas antes de la llamada y continuas tu trabajo.

Procedimientos Basicos de Ramificacion

Imagina que estas trabajando en un proyecto y tienes algunos commits realizados. Decides trabajar en el problema #53. Para crear una nueva rama y saltar a ella, en un solo paso utilizamos git checkout -b:

git checkout -b iss53
Switched to a new branch "iss53"

Comparemos ahora el Antes y Despues:

Diagrama de commits (1)
Diagrama de commits (2)

git checkout -b <rama> es un atajo para git branch <rama> y luego git checkout <rama>.


Trabajas en el sitio web y haces algunos commits. Con ello avanzas en la rama iss53, que es la que tienes activa en este momento.

nvim index.html # Editas un archivo
git commit -am 'added a new footer [issue 53]'

Entonces, recibes la llamada avisandote de otro problema urgente en el sitio web y debes resolverlo inmediatamente. Al usar Git, no necesitas mezclar el nuevo problema con los cambios que ya habias realizado sobre el problema #53, ni tampoco perder tiempo revirtiendo esos cambios para podeer trabajar sobre el contenido que esta en produccion.

Diagrama de commits (3)

Basta con saltar de nuevo a master y seguir trabajando desde ahi. Pero, antes de poder hacer eso, debemos tomar en cuenta que si tenemos cambios aun no confirmados en el directorio de trabajo o en el area de preparacion, Git no nos permitira saltar a otra rama con la que podriamos tener conflictos.


Lo mejor siempre es tener un estado de trabajo limpio y despejado antes de saltar entre ramas.


Entonces con todo confirmado, cambiamos a la rama master:

git checkout master
Switched to branch 'master'

Recordemos que Git añade, quita y modifica archivos automaticamente para asegurar que tu copia de trabajo luce exactamente como lucia en la rama en la ultima confirmacion.


A continuacion, es momento de resolver el problema urgente. Vamos a crear una nueva rama hotfix, sobre la que trabajar hasta resolverlo:

git checkout -b hotfix
Switched to a new branch 'hotfix'
vim index.html
git commit -am 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
 1 file changed, 2 insertions(+)

Ahora puedes incorporar los cambios a la rama master para ponerlos en produccion.

Diagrama de commits (4)

Para fusionar se utiliza el comando git merge:

git checkout master         # Cambiar a la rama master
git merge hotfix            # Fusionar hotfix a la rama actual
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

Notar que se utilizo el metodo Fast-forward, esto significa que mueve los commits divergentes de la rama hotfix adelante de los commits de la rama master. Comparemos Antes de fusionar vs Despues de fusionar:

Diagrama de commits (4)
Diagrama de commits (5)

Luego de haber resuelto el problema urgente, podemos volver donde estabamos trabajando. Pero antes, eliminemos la rama hotfix que ya no nos va a servir mas. Para esto usamos git branch -d:

git branch -d hotfix
Deleted branch hotfix (3a0874c).

Ahora volvamos al trabajo:

git checkout iss53 # Cambiar a iss53
Switched to branch "iss53"
vim index.html # Editar index.html
git commit -am 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

Pero hay un detalle, los cambios fusionados de hotfix no existen en la rama iss53.

Diagrama de commits (6)

Procedimientos Basicos de Fusion

Supongamos que tu trabajo con el problema #53 ya esta completo y listo para fusionarlo. Asi que procedemos a utilizar git merge:

git checkout master # Cambiar a master
Switched to branch 'master'
git merge iss53 # Fusionar iss53 a la rama actual
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

Esta fusion es diferente a la de hotfix -> master, como podemos ver esta utiliza recursive en vez de fast-forward. Dado que la confirmacion en la rama actual no es ancestro directo de la rama que pretendemos fusionar, Git tiene cierto trabajo extra que hacer.


En lugar de simplemente avanzar la referencia de la rama, Git crea una snapshot resultante de la fusion, y crea automaticamente un nuevo commit que apunta a ella. A este proceso se le conoce como merge commit (fusion confirmada) y su particularidad es que tiene mas de un padre.

Diagrama de commits (1) (merge)
Diagrama de commits (2) (merge)

Git automaticamente determina cual es el mejor ancestro comun para realizar la fusion. Ahora que todo tu trabajo ya esta fusionado, podemos eliminar iss53 con:

git branch -d iss53

Principales Conflictos que pueden surgir en las fusiones

En ocasiones, los procesos de fusion no suelen ser fluidos. Si hiciste cambios en la misma parte de un archivo en las dos ramas distintas que pretendes fusionar, Git no sera capaz de fusionarlas directamente y producira un conflicto:

git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Git pausa la fusion y espera a que resuelvas el conflicto. Puedes ver que archivos estan sin fusionar en cualquier momento tras un conflicto con git status:

git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

Como leer un conflicto

Git añade marcadores de resolucion de conflictos en los archivos con problemas. El archivo afectado tendra una seccion similar a esta:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
  please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
  • La seccion entre <<<<<<< HEAD y ======= es la version en tu rama actual (master).
  • La seccion entre ======= y >>>>>>> iss53 es la version de la rama que estas fusionando.

Resolver el conflicto

Debes elegir una de las dos versiones, o combinarlas manualmente. Por ejemplo, podrias reemplazar todo el bloque anterior por esto:

<div id="footer">
  please contact us at email.support@github.com
</div>

Una vez resueltos todos los conflictos, ejecuta git add sobre cada archivo afectado para marcarlo como resuelto. Luego confirma la fusion:

git add index.html
git commit

Git sabe que archivos tenian conflictos y los marca automaticamente al abrir el editor de confirmacion.


Gestion de Ramas

El comando git branch tiene mas funciones que solo crear y borrar ramas. Si lo ejecutas sin parametros, obtienes una lista de todas las ramas del proyecto:

git branch
  iss53
* master
  testing

El caracter * delante de master indica la rama activa en este momento (la que apunta HEAD). Si haces un commit ahora, esa sera la rama que avance. Para ver el ultimo commit de cada rama, puedes usar git branch -v:

git branch -v
  iss53    93b412c fix javascript issue
* master   7a98805 Merge branch 'iss53'
  testing  782fd34 add scott to the author list in the readmes

Otra opcion util es filtrar las ramas segun si ya han sido fusionadas o no con la rama activa. Para ver las ramas ya fusionadas con la rama actual:

git branch --merged
  iss53
* master

La rama iss53 aparece porque ya fue fusionada. Las ramas que no llevan el * delante pueden eliminarse sin problemas con git branch -d, ya que su contenido ya esta incorporado. Para ver las ramas con trabajo aun sin fusionar:

git branch --no-merged
  testing

Si intentas borrar una rama con trabajo sin fusionar, Git te dara un error:

git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

Si realmente deseas borrarla y perder ese trabajo, puedes forzar el borrado con git branch -D.


Recursos