Tabla de Contenidos
Branching y Merging
Módulo perteneciente al curso Git: Control de código fuente para programadores
Una rama o branch es una bifurcación dentro del historial de código que nos permite gestionar los cambios de los mismos archivos en dos historias paralelas. Las ramas en Git, al contrario que en otros VCS, son extremadamente ágiles y livianas y no penalizan ni el almacenamiento ni la velocidad. Por eso en Git se recomienda usar tantas ramas como creamos necesarias sin preocuparnos por su impacto.
De este modo, por ejemplo, en un proyecto es posible abrir una rama nueva por cada característica diferente de la aplicación en la que se esté trabajando, sin miedo a interferir en el trabajo de otras personas ni “ensuciar” la rama “maestra” con código a medio terminar.
Gracias a esta característica podríamos tener la rama master (u otra) con el código estable, tal cual está en el servidor para los clientes, e ir desarrollando cosas nuevas en otras ramas. Si de repente se produce un error en producción que tenemos que arreglar, podemos cambiarnos a la rama master y abrir desde ella otra rama exclusivamente para resolver el bug, sabiendo que en ella partimos del código exacto que hay en producción y que no nos interferirán los nuevos desarrollos que tengamos en marcha:
Una vez terminado de arreglar el bug y habiéndolo probado bien, podemos mezclar la rama de nuevo en nuestro master para almacenar el estado final, e incluso deshacernos de la rama que ya no necesitamos:
Al mismo tiempo, la otra rama, en la que se está desarrollando la característica grande, puede haber evolucionado por su cuenta, totalmente ajena a los cambios que se han hecho en otras ramas, como la que hemos utilizado para solucionar el bug, hasta que la mezclemos con el master u otra rama.
En este módulo vamos a presentar las ramas y la funcionalidad básica de éstas que se utiliza en el día a día.
Introducción a las ramas y sus conceptos principales
Hasta ahora hemos aprendido a registrar cambios haciendo commits directamente sobre el master, aunque no entramos a definirlo todavía para no complicarte más. Entonces, ¿qué es el master cuando trabajamos con Git?
El master es el branch (o rama) por defecto de Git.
Cuando inicias un repositorio en Git, creas un repositorio vacío. Al registrar tu primer cambio ocurren tres cosas:
- Se crea el branch principal de Git llamado por defecto
master - Se actualiza un puntero llamado
HEADque es lo que indica a Git en dónde estamos situados dentro del repositorio. - Se registra ese primer commit sobre la rama
master
Por regla general, a master se la considera la rama principal y la raíz de la mayoría de las demás ramas. Lo más habitual es que en master se encuentre el “código definitivo”, que luego va a producción. Es la rama en la que se mezclan todas las demás, tarde o temprano, para dar por finalizada una tarea e incorporarla al producto final, aunque no siempre es así.
Como ya hemos comentado, en un repositorio puedes tener un número ilimitado de ramas, que puedes crear por cada característica en desarrollo, bug, etc…:
Estos branches adicionales son muy útiles para trabajar en tu propio entorno sin que nadie te moleste, evitando, en la medida de lo posible, los famosos conflictos que veremos un poco más adelante en este mismo módulo.
Grafo de commits
Git recoge en su histórico los diferentes commits que has realizado. Cada commit se relaciona con los demás mediante un puntero, único, que apunta hacia atrás de modo que se forma un grafo. El histórico de Git no es más que un conjunto de commits relacionados de esta manera y la representación más correcta de un repositorio y sus ramas debería ser más bien así:
Como puedes observar, cada commit apunta siempre hacia el commit anterior que le precede y solamente hacia éste. Analizando estas relaciones es fácil determinar las rutas/ramas que sigue el histórico dentro del repositorio de código.
Un commit no puede apuntar hacia más de un commit anterior, aunque un commit sí que puede recibir referencias de más de un commit, como se ve en la figura anterior en las ramas. Por ejemplo, el primer commit de master, a la izquierda, recibe referencias desde otros dos commits, el siguiente en master y el primero de la rama llamada “Característica pequeña”.
Una rama simplemente es un nombre otorgado a una de estas bifurcaciones para poder trabajar con esa historia paralela en concreto. Moverse entre ramas implica solamente cambiarse al commit apropiado.
El concepto de HEAD
En Git se denomina HEAD al commit en el que está tu repositorio posicionado en cada momento.
Por regla general HEAD suele coincidir con el último commit de la rama en la que estés, ya que habitualmente estás trabajando en lo último. Pero si te mueves hacia cualquier otro commit anterior, entonces el HEAD estará más atrás.
A este estado, cuando HEAD no está en el último commit de la rama, se le denomina “Detached HEAD” o “HEAD desvinculado” en español (aunque raramente lo verás utilizado en este idioma).
Siempre es posible referirse al commit actual usando el nombre HEAD y, de hecho, es posible referirse a otros commits usando su relación con HEAD de forma relativa.
Así, por ejemplo, puedes referirte a un commit que está dos por detrás del actual usando la expresión HEAD~2 (es una “virgulilla”, no un “menos”):
git log HEAD~2
Con él se ven los detalles de todos los commit a partir del “abuelo” del commit actual (2 por detrás).
El símbolo ~ siempre sigue al primer “padre” del commit referenciado (el HEAD en nuestro ejemplo). Pero si existe más de una rama que parte de un commit, podemos movernos por éstas usando el símbolo ^ para indicar el orden de las mismas. Por ejemplo: HEAD^2 obtendría el primer commit de la segunda rama que parte del commit en el que está HEAD en ese momento.
Se pueden combinar ambos para moverse por las ramas. Así, HEAD^2~3 indica que se obtendrá el tercer commit a partir del primer commit de la segunda rama que parte del commit actual, es decir, 4 commits hacia atrás por la segunda rama (el primer ^ se refiere al primer commit hacia atrás por esa rama y luego indicamos otros 3).
Puede parecer lioso, pero no lo es tanto si lo ves en un diagrama. Este nos muestra todas las formas de llegar al commit A desde los distintos commits existentes en el grafo:
Como ves, estas referencias relativas valen para cualquier commit, no solo para HEAD, y se referencia a partir de su hash (en la figura, por simplicidad hemos usado letras).
Trabajando sobre master
El flujo de trabajo que hemos aprendido hasta ahora se denomina comúnmente “trabajar sobre master”, cuando todos los commits se hacen sobre la rama de dicho nombre, que es la rama por defecto. Si trabajas sobre master significa que todos los cambios los vas a registrar directamente sobre la rama master, sin crear ramas diferentes.
Esta forma de trabajar se usa muchas veces con proyectos personales o de poca complejidad, en los que sabes, de antemano, que será raro coincidir con otro desarrollador subiendo cambios.
Ventajas
Es un flujo muy fácil de aprender y muy rápido de ejecutar. Básicamente lo que necesitas es crear el repositorio y empezar a trabajar con lo que ya conoces.
Desventajas
No es apto, en general, para proyectos grandes, con más de un desarrollador o que necesiten de un sistema de despliegue y versiones más o menos serio.
Trabajo con ramas
La forma habitual de trabajar en equipo con Git es haciendo uso de los branches. Si trabajas directamente sobre el master con varias personas, te encontrarás, más temprano que tarde, con un conflicto en algún archivo que te impedirá subirlo a tu repositorio y continuar trabajando.
Las ramas también tienen un beneficio muy interesante a la hora de usarlas y, es que, por naturaleza, a nosotros los desarrolladores no nos gusta que nos toquen nuestras cosas mientras trabajamos (¡y menos otro desarrollador!). Al crear un branch local estamos creando un entorno de desarrollo propio donde sólo nosotros registramos cambios que más adelante llevaremos al master a través de un merge.
Nota: otra opción puede ser que trabajes con muchos desarrolladores en un mismo equipo y que unos pocos queráis trabajar en un desarrollo aparte. Lo que tendrías que hacer sería tomar ese branch local y subirlo al repositorio remoto para compartirlo transformándolo en un branch remoto al que todo el equipo tuviese acceso.
Las ramas son un recurso muy útil en Git. En próximas lecciones y módulos aprenderemos algunos de los flujos más comunes y adaptativos que hay basados en este sistema.
Trabajando con ramas
Muy bien, ya nos hemos decidido a empezar a trabajar con ramas ¿y ahora qué? Pues, simplemente hay que saber cómo crearlas y cómo cambiar de unas a otras.
Crear tus propios branches
Para crear una rama el comando apropiado es:
git branch nombre_branch
Y con esto ya tendríamos nuestra rama creada, ¡así de sencillo!:
Fíjate especialmente en cómo coincide en el mismo commit tanto el HEAD, como el master, como la rama nueva. Pero el hecho de que crees la rama NO quiere decir que puedas trabajar ya en ella.
Por defecto, cuando creas una rama sólo la creas, pero no te mueves a ella para trabajar. Eso se hace con el comando git-checkout:
git checkout nombre_branch
Compara ahora las dos últimas imágenes y fíjate especialmente en el puntero HEAD (el que se muestra entre paréntesis en color azul, al final de cada línea de comandos), para ver a dónde está apuntando. Esto indica que estamos en la nueva rama que hemos creado, y que podemos empezar a trabajar en ella usando el flujo que hemos aprendido antes, git-add y git-commit.
Nota: como ya hemos dicho, el flujo de git-add y git-commit es básico para cualquier paso en Git y el resto de flujos parten de él (incluido este que estamos aprendiendo con las ramas).
Otra opción para crear una rama y cambiarnos a ella es ejecutar directamente el comando git-checkout con el modificador -b:
git checkout -b nombre_branch
Con la opción -b le indicamos a Git que, si no existe esa rama, nos la cree y adicionalmente nos mueva a ésta. Dicho de otra forma, hace que el HEAD apunte a la nueva rama.
El comando git-checkout también sirve para mover el HEAD a cualquier otro commit. Si especificas el hash de un commit determinado, por ejemplo git checkout 6e86 (normalmemente llega con los 4 primeros caracteres para determinarlo) moverás el HEAD a ese commit en concreto y quedará en estado “detached”, como ya hemos comentado, porque no está en el último de la rama. El propio Git te avisará de este hecho y te informará sobre cómo crear una rama a partir de ese commit si hace algún cambio. Puedes volver a poner el HEAD en el último y dejar ese estado usando un simple git checkout nombreRama.
Una vez que estamos en una rama, podemos trabajar en ella como veníamos haciendo hasta ahora en master, solo que en una rama diferente. Así, añadiremos cambios al área de staging, crearemos commits para generar versiones concretas y podremos hacer push y pull (¡y fetch!) con el repositorio remoto si lo consideramos necesario. La única diferencia es que los commits se almacenarán en esa rama y no en la principal. Pero los principios de trabajo son exactamente los mismos.
De todos modos, las ramas tienen un par de particularidades que necesitamos aprender:
- Cómo actualizar nuestra rama con cambios posteriores que suban otras personas desde la rama de la que hemos partido, si es que los necesitamos para estar al día (
rebase). - Cómo llevar nuestros cambios a master o a otras ramas una vez finalicemos de trabajar en ella (
merge).
Vamos a verlo.
Actualizar nuestro branch con rebase
Tenemos la siguiente situación con dos ramas, Característica Pequeña y Característica Grande:
Como puedes ver, entre estas dos ramas hay varios commits de gente (o nosotros mismos) que ha ido actualizando código. Pongámonos en la siguiente situación: estamos desarrollando algo en la rama Característica Pequeña, pero para continuar necesitamos de un desarrollo que ha subido un compañero a master, que va por delante de nosotros. Una situación bastante común.
Lo que debemos hacer es actualizar nuestra rama con lo que se conoce como rebase.
El concepto es muy sencillo. Simplemente aplicamos los commits de nuestra rama sobre la rama con la que queremos actualizarnos, usando el comando git-rebase:
git rebase rama_destino rama_final
Este comando se ejecuta estando en la rama que vamos a actualizar. En este caso Característica pequeña:
git rebase master "Característica pequeña"
Y lo que conseguimos ejecutando este comando es actualizar nuestra rama con los commits posteriores de la rama que definamos como destino, en este caso master:
La segunda opción del comando, lo que denominamos branch_final, sirve para que, una vez terminado el rebase nos quedemos en la rama que le especifiquemos, sino automáticamente haríamos un checkout a la rama de destino.
Y con esto tendríamos actualizada nuestra rama, sin movernos de ella, y podríamos seguir trabajando con los commits que necesitábamos del master.
Mezclar las ramas
Ya podemos actualizar nuestro branch con los cambios de los demás si lo necesitamos, pero nos queda saber cómo incorporar los cambios de nuestra rama a master para que sean visibles para el resto. Esta acción de actualizar el master (o cualquier otra rama) con cambios de otra rama se denomina Merge o Mezcla y se lleva a cabo con el comando git-merge.
git merge nombre_branch
Fíjate cómo lo aplicamos en la siguiente imagen:
En este caso tenemos la rama “nuevo_branch” con un cambio que no tenía el master, por eso aparece por delante. Lo que se ha hecho para actualizar el master ha sido lo siguiente:
git checkout master: necesario para ir al master, ya que es la rama que vamos a actualizar.git merge nuevo_branch: hacemos unmergede la rama local en el master.
Es muy importante recalcar que para hacer la mezcla de dos ramas siempre debemos movernos primero a la rama que vamos a actualizar, indicando la otra rama a mezclar en el comando git-merge.
Una vez hecho el merge fíjate que tanto el master como la rama “branch_local” se encuentran situados en el mismo commit, actualizados.
Como ya hemos terminado el desarrollo y lo hemos mezclado desde la rama que estábamos utilizando, no es necesario mantener esa rama local. Para eliminarla sólo debemos ejecutar:
git branch -d nombre_rama
Con esto eliminamos las ramas del repo local. Para eliminar una rama del repositorio remoto si la habíamos sincronizado antes, hay que añadir la opción -r que se puede combinar con la anterior como -rd:
git branch -rd nombre_branch
Conclusión
Con esto ya tenemos todo lo necesario para crear flujos de trabajo medianamente complejos, muy útiles para cualquier proyecto. Las ramas están pensadas para trabajar en grupo, pero esto no impide que de vez en cuando puedan ocurrir conflictos en el repositorio. En próximas lecciones veremos cómo se generan y cómo resolverlos.
Resolución de conflictos en Git
Hemos aprendido los conceptos básicos de Git con los que poder trabajar en cualquier repositorio y con cualquier equipo. Eso sí, si todo va bien. Pero a veces las cosas no salen como uno se lo espera y resulta que cuando vas a subir tus cambios al master alguien ha tocado el mismo archivo en la misma línea que tú y la cosa se empieza complicar. Entonces surgen los conflictos.
Esta situación ocurre muchas veces, sobre todo con equipos grandes ya que, como es normal, la gente está trabajando y el código evoluciona. Un conflicto sucede cuando dos personas realizan una modificación sobre el mismo punto en un archivo. Por lo general, Git es muy bueno gestionando los conflictos, pero es cierto que a veces es indispensable la interacción del desarrollador para resolverlos.
Pongámonos en la siguiente situación. Tenemos el siguiente archivo HTML:
<!DOCTYPE html> <html> <head> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> <title>Modulo 2</title> </head> <body> </body> </html>
En paralelo, en su repositorio, otra persona del equipo modifica la etiqueta <title> para poner “Modulo 3”. Y tú en tu propia rama modificas también el contenido de la misma etiqueta y escribes “Modulo 3 - Branches”. Esto genera un conflicto ya que, como puedes ver, ninguno está mal y Git no puede discernir cuál de las modificaciones es la correcta.
Ahora, al ejecutar el comando git-merge Git iniciará el proceso llamado de resolución de conflictos, que consta de los siguientes pasos:
- Git detecta un conflicto que no puede resolver automáticamente y entra en el proceso de resolución de conflictos.
- Git añade a la zona de trabajo los archivos que debes revisar, con los conflictos que se generan.
- Debes modificar los archivos notificados por Git, arreglándolos de la manera que veas conveniente.
- Una vez editados y con las resoluciones ya decididas, los añades a la zona de preparación con
git-add. - Antes de terminar el proceso puedes utilizar el comando
git-diffpara estudiar los cambios realizados:
git diff branch_origen branch_destino
Una vez finalizado los cambios podemos optar por dos comandos:
git commit -a -m "mensaje_commit"
Que indica a Git que debe confirmar los cambios de los archivos que se han modificado o eliminado (el -a), y nos permitirá continuar con el git-merge.
O bien:
git merge --abort
Lo que impedirá que se lleve a cabo el git-merge y nos dejará en la situación anterior a la de intentar subir los cambios.
Conclusión
En general, la resolución de conflictos no es trivial y requiere dedicarle tiempo para resolverlos. git-diff es un comando que ayuda mucho a la hora de tratar con ellos, pero al final vas a tener que abrir los archivos y tocar el código para dejar los archivos tal y como deberían quedar finalmente.
Vamos a verlo en la práctica…
Prácticas propuestas para el módulo
En este módulo hemos añadido conceptos tremendamente importantes para trabajar con Git. Al igual que en el módulo anterior, hemos aprendido un flujo de trabajo relativamente complejo en el que creamos ramas para aislar nuestros desarrollos. Haremos merge de esas ramas resolviendo los conflictos que encontremos por el camino, e iremos añadiendo poco a poco este flujo de trabajo en nuestro repositorio para enriquecer el trabajo del equipo.
Al igual que en el caso anterior, es fundamental practicar los conceptos aprendidos o te vas a perder muchos detalles importantes:
- Branch
- Merge
- Rebase
Para ellos sería interesante que, como mínimo, aplicases estos pasos (aunque te recomiendo experimentar todas las situaciones que te imagines en un repositorio local):
- Aprovecha el repositorio que creaste del módulo anterior y crea una rama.
- En esta nueva rama realiza una modificación sobre el archivo
index.html. - Registra dicha modificación (
commit). - Muévete al master.
- Realiza un cambio en un lugar distinto al que hiciste en el punto 2.
- Sube los cambios de la otra rama al master.
- Elimina la rama local que has creado.
- Repite los pasos 1, 2, 3 y 4.
- Para forzar un conflicto, realiza una modificación en un punto de otra rama donde haya otra modificación previa.
- Registra el cambio en el master.
- Intenta hacer el
merge. - Resuelve finalmente el conflicto como creas conveniente.










