4 Comandos (más) para aprender a usar Git (capítulo II)

4 Comandos (más) para aprender a usar Git (capítulo II)

Branches, Merges y otras yerbas

En el capítulo I sobre herramientas de desarrollo de software, empezamos a trabajar con Git, un sistema de control de versiones muy poderoso y ampliamente utilizado actualmente.

En éste capítulo vamos a profundizar en algunos comandos y conceptos relacionados a los branches, para poder sacarle el jugo a Git trabajando localmente (locamente también jeje).

El (nuevo) problema

Ivy desarrolló su primer sitio web para una clienta y lo puso online. Usuarios, clienta y desarrolladora están contentas con el resultado. El entusiasmo derivó en un nuevo pedido a Ivy para agregar más funcionalidades al sitio. Cuando está trabajando en la primera funcionalidad del nuevo encargo, le avisan que hay una funcionalidad en el sitio online que no hace lo que esperaban que haga (bug) y necesitan que lo solucione lo antes posible. La idea es no tener que poner online la funcionalidad que tiene por la mitad al arreglar el bug. Se tiene que arreglar sin agregar nada nuevo, para no comprometer la estabilidad del sitio.

¿Cómo hace Ivy para volver a la versión del sitio online, arreglar el bug y retomar la funcionalidad que venía implementando sin romper nada? De nuevo, una solución rápida podría ser mantener copias, pero como vimos en el capítulo anterior, rápidamente se vuelve inmanejable. Más aún si ahora agregamos lo que hay online. Otra opción es poner online lo que tiene por la mitad tratando de no romper nada, pero compromete la estabilidad del sitio y tarde o temprano ésta estrategia siempre termina rompiendo más de lo que arregla.

Branches al rescate!

Hasta ahora estuvimos haciendo commits sobre master. Pero para resolver éste problema, vamos a tener que crear nuevos branches, saltar entre ellos y volverlos a juntar. Git ofrece todo ésto (y mucho más! yay!).

Supongamos el siguiente escenario:

   c0 -- c1 -- c2 -- c3    <--- master

Cuando Ivy terminó de desarrollar el sitio, el último commit que metió fue c3 en el branch master, y usó esa versión para ponerlo online.

Creando un branch

Ahora, para la nueva funcionalidad (llamémosla funcionalidad1, si, no me caracterizo por ser creativo con los nombres), crea un nuevo branch desde ese commit, usando el comando branch:

$ git status
On branch master
nothing to commit, working tree clean

$ git branch funcionalidad1
$

Esto creó el branch, no muestra ningún mensaje una vez creado, pero lo podemos ver usando git branch sin nada más:

$ git branch
* master
 funcionalidad1

Con git branch <nombre> creamos branches nuevos, tomando como referencia el branch donde estamos actualmente (en éste caso, master).

Pero todavía estamos en master. Hay que decirle a Git que queremos empezar a trabajar en funcionalidad1 ahora.

Pasando de un branch a otro

Para eso usamos el comando checkout, así:

$ git checkout funcionalidad1
Switched to branch 'funcionalidad1'

$ git status
On branch funcionalidad1
nothing to commit, working tree clean

$ git branch
* funcionalidad1
master

Con git checkout <nombre> nos movemos al branch llamado nombre.

Nota mental: Hay una forma simplificada de hacer todo junto, crear el branch y moverse al mismo, con un sólo comando: git checkout -b <nombre>.

Trabajando en distintos branches a la vez

Ahora el escenario es éste:

   c0 -- c1 -- c2 -- c3    <--- master y funcionalidad1

Después de trabajar un poco en esa funcionalidad, agrega varios commits en ese branch y queda así:

c0 -- c1 -- c2 -- c3    <--- master
                    \
                     c4 -- c5 -- c6    <--- funcionalidad1

Cada commit tiene una referencia al anterior (parent commit) y siempre el "branch", el nombre que usamos para referirnos a él en los comandos, apunta al último commit. En el caso de master, sigue apuntando a c3 y funcionalidad1 ahora apunta a c6.

Cuando la clienta le avisa del problema en el sitio online, Ivy puede saltar al branch master, dejando la funcionalidad 1 a medio implementar, sin miedo a perder lo que hizo, o a que algo de eso salga online antes de tiempo.

$ git checkout master
Switched to branch 'master'

Nota mental: sólo se puede saltar entre branches si no hay cambios pendientes de commitear que tengan conflictos con el código en el destino. Lo mejor por ahora es tener todo commiteado. Más adelante podemos ver otra forma de guardar los cambios temporalmente sin commitear (con el comando stash).

Entonces Ivy pudo pasar al branch master, agregar un commit que arregla el bug y ponerlo online. El estado del repositorio queda así:

c0 -- c1 -- c2 -- c3 -- c7   <--- master (Ivy está acá)
                    \
                     c4 -- c5 -- c6    <--- funcionalidad1

Para volver al branch de la funcionalidad 1, puede hacer lo mismo:

$ git checkout funcionalidad1
Switched to branch 'funcionalidad1'

Este branch donde está ahora no tiene el arreglo del bug, porque éste sólo está en master. Si lo necesita para seguir trabajando, puede traerse los cambios de master a su branch. Cómo lo hacemos?

El comando merge

El escenario más simple podría ser así:

$ git merge master
Updating c6..c7
Fast-forward
 hola_mundo.html | 4 +++-

El comando merge se usa estando en el branch donde queremos poner los cambios. Y le tenemos que pasar como parámetro el branch de donde queremos que git tome los cambios. Así es como nos paramos sobre funcionalidad1, y le pedimos que nos traiga lo de master. Lo que sigue es un "fast-forward", el merge se hace automáticamente y todos contentos, ya podría seguir trabajando con todos los cambios en su branch funcionalidad1.

Pero a veces las cosas no salen taaan fácil. Es viernes a la tarde y los sistemas suelen "romperse" de forma imprevisible. Así que vamos a probar un ejemplo un poco más complejo. El escenario donde el merge no sale automáticamente (prometo que sobre el final del artículo vamos a mostrar el caso más simple).

Merge con problemitas

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

En éste caso, git no puede traerse los cambios automáticamente porque la versión de hola_mundo.html en master y en funcionalidad1 tienen cambios en los mismos lugares. Git no sabe cuál tomar y cuál descartar, así que nos pide que le ayudemos.

Merge conflicts!

El archivo se ve así:

<html>
  <head>
  </head>
  <body>
    <p>hola mundo!</p>
    <p>esta es la versión 4!</p>
  </body>
  <script>
<<<<<<< HEAD
    // acá van las funciones de Ivy para su sitio web
    // acá agrega algo de código
    // acá también agrega código
 </script>
=======
    // acá van algunas de las funciones de Ivy para su sitio web
    // por las dudas agrega algo acá
  </script>
>>>>>>> master
</html>

Se puede ver que se agregaron unos chirimbolos ahí que no son parte del html original. Es la forma que tiene Git de decirnos dónde está el problema, y qué pertenece a qué branch.

Entre <<<<<<< HEAD y ======= están los cambios que tenemos nosotros en nuestro branch actual. Entre ======= y >>>>>>> master tenemos los cambios que estamos trayendo desde el otro branch.

Acá lo único que queda por hacer es mezclar los cambios manualmente. Ivy tiene el contexto de qué es qué y cómo funciona cada cosa. Así que ella junta todo, elimina las líneas con chirimbolos y verifica que funcione bien antes de terminar el proceso.

$ git status
On branch funcionalidad1
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

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

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

Hay que decirle a git que ya resolvimos los problemas y que puede terminar el merge:

$ git add hola_mundo.html
$ git status
On branch funcionalidad1
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
    modified:   hola_mundo.html

$ git commit
[funcionalidad1 c8] Merge branch 'master' into funcionalidad1
$ git status
On branch funcionalidad1
nothing to commit, working tree clean

Ahora los branches quedan así:

c0 -- c1 -- c2 -- c3 -------------- c7
                    \                 \
                     c4 -- c5 -- c6 -- c8   <--- funcionalidad1 (Ivy) y master

Lo que sigue es simplemente el uso de los mismos comandos que vimos para seguir agregando commits (c9 y c10) al branch funcionalidad1, y eventualmente hacer un merge a master para que desde ahí pueda poner la nueva funcionalidad online. El merge en ese caso debería ser automático:

$ git checkout master
Switched to branch 'master'
$ git merge funcionalidad1
Updating c7..c10
Fast-forward
 hola_mundo.html | 4 +++-

Los branches terminarían quedando algo así:

c0 -- c1 -- c2 -- c3 -------------- c7 --------------- c11  <-- master
                    \                 \               /
                     c4 -- c5 -- c6 -- c8 -- c9 -- c10

Y qué hago con los branches que uso para las funcionalidades una vez que ya está el código en master y online?

Borrando branches que no necesito más

Se usa un parámetro en el comando branch:

$ git branch --delete funcionalidad1
Deleted branch funcionalidad1 (was c11).

Nota mental: es importante destacar que uno no puede borrar el branch donde está parado en ese momento, así que para borrar funcionalidad1, tengo que estar en master.

Listo. Con éstos comandos ya es posible trabajar con varias versiones a la vez sin romper nada. Pensaba hablar de cómo deshacer cambios (rollback), pero es casi como para un artículo propio. Así que queda para más adelante.

Lo que viene

En el próximo artículo vamos a ver cómo trabajar en equipo con Git: Repos remotos, Push/Pull, Pull Requests y más.