Notxor tiene un blog

Defenestrando la vida


Cómo estoy utilizando git (II)

Hoy voy con la segunda parte, que no entró en el artículo de ayer. El tema iba con las ramas y los repositorios. Hasta ahora todo ha ido bien, entre otras cosas porque sólo se han hecho las modificaciones desde un repositorio y así nunca hay conflictos, pero ¿qué ocurre si hay un conflicto al hacer un commit?

Conflictos en el repositorio

Para simularlo he modificado en otro ordenador, con otra copia del repositorio el archivo2.txt y para generar el conflicto modificaré el mismo fichero en la copia local. De momento, mi repositorio local no se ha enterado de los cambios.

~/proyectos/proyecto-nuevo> git status
En la rama master
Tu rama está actualizada con 'origin/master'.

nada para hacer comit, el arbol de trabajo esta limpio
~/proyectos/proyecto-nuevo> git checkout prueba 
Cambiado a rama 'prueba'
Tu rama está actualizada con 'origin/prueba'.
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama está actualizada con 'origin/prueba'.

nada para hacer comit, el arbol de trabajo esta limpio

Ninguna de las dos ramas se han dado cuenta que el remoto ha cambiado. Si hago modificaciones terminaré encontrando el problema.

~/proyectos/proyecto-nuevo> echo "Cambios en el repositorio local." >> archivo2.txt 
~/proyectos/proyecto-nuevo> git status                                                                 
En la rama prueba                                                                                                    
Tu rama está actualizada con 'origin/prueba'.                                                                        

Cambios no rastreados para el commit:                                                                                
  (usa "git add <archivo>..." para actualizar lo que será confirmado)                                                
  (usa "git checkout -- <archivo>..." para descartar los cambios en el directorio de trabajo)                        

	modificado:     archivo2.txt                                                                                 

sin cambios agregados a la confirmación (usa "git add" y/o "git commit -a")                                          
~/proyectos/proyecto-nuevo> git add archivo2.txt                                                       
~/proyectos/proyecto-nuevo> git commit -m "Cambios en local pretendiendo que haya conflictos."         
[prueba 8f8437c] Cambios en local pretendiendo que haya conflictos.                                                  
 1 file changed, 1 insertion(+)                                                                                      
~/proyectos/proyecto-nuevo> git push                                                                   
To /ruta/a-la/nube/proyecto-nuevo.git                                                                              
 ! [rejected]        prueba -> prueba (fetch first)
error: fallo el push de algunas referencias a '/ruta/a-la/nube/proyecto-nuevo.git'
ayuda: Actualizaciones fueron rechazadas porque el remoto contiene trabajo que
ayuda: no existe localmente. Esto es causado usuallmente por otro repositorio 
ayuda: realizando push a la misma ref. Quizás quiera integrar primero los cambios
ayuda: remotos (ejem. 'git pull ...') antes de volver a hacer push.
ayuda: Vea 'Notes about fast-forwards0 en 'git push --help' para detalles.

Bien, todo parecía ir bien y al hacer el push para subir los cambios al remoto, se queja. Rechaza el cambio que he hecho, me dice que hay cambios que no están en el local y me recomienda que primero actualice mi copia de trabajo con esos cambios. Sus deseos son órdenes.

~/proyectos/proyecto-nuevo> git fetch origin
remote: Contando objetos: 3, listo.
remote: Comprimiendo objetos: 100% (3/3), listo.
remote: Total 3 (delta 1), reused 0 (delta 0)
Desempaquetando objetos: 100% (3/3), listo.
Desde /ruta/a-la/nube/proyecto-nuevo
   a60c91a..bf825fc  prueba     -> origin/prueba
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama y 'origin/prueba' han divergido,
y tienen 1 y 1 commits diferentes cada una respectivamente.
  (usa "git pull" para fusionar la rama remota en la tuya)

nada para hacer comit, el arbol de trabajo esta limpio

He utilizado el comando habitual de actualizar el repositorio local: fetch. Sin embargo, tras hacerlo git sigue pidiéndome que fusione la rama remota con la local haciendo un git pull.

~/proyectos/proyecto-nuevo> git pull
Auto-fusionando archivo2.txt
CONFLICTO (contenido): Conflicto de fusión en archivo2.txt
Fusión automática falló; arregle los conflictos y luego realice un commit con el resultado.
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama y 'origin/prueba' han divergido,
y tienen 1 y 1 commits diferentes cada una respectivamente.
  (usa "git pull" para fusionar la rama remota en la tuya)

tienes rutas no fusionadas
  (arregla los conflictos y corre "git commit"
  (usa "git merge --abort" para abortar la fusion)

rutas no fusionadas:
  (usa "git add <archivo>..." para marcar una resolucion)

	ambos modificados:     archivo2.txt

sin cambios agregados a la confirmación (usa "git add" y/o "git commit -a")

El conflicto se ha manifestado. Podemos volvernos atrás y renunciar al merge, en ese caso perderemos nuestros cambios. Lo habitual en estos casos es mediar en el conflicto de los dos repositorios.

Arreglar conflictos a mano

Todo conflicto me pone nervioso y me hace preguntarme si seré capaz de arreglarlo. Luego recuerdo que todo tiene solución y la solución habitual es enfrentarse al problema: abrimos el archivo en conflicto con nuestro editor favorito (en mi caso utilizo emacs).

captura-conflicto-emacs image

Figura 1: Editando el fichero conflictivo con emacs.

Como se puede ver en la imagen. Se marcan los cambios entre dos líneas, la primera marcada como <<<<<<< HEAD y la última comienza con una serie de caracteres >>>>>>>. Esas dos líneas marcan la amplitud del conflicto. Lo situado entre la primera y otra línea marcada con una serie de caracteres «=» son los cambios que tengo en local y lo que está entre los «=» y la última es lo que está en el remoto. El hash que muestra es el identificador del commit donde están los cambios.

Vale, pero ¿cómo arreglo el conflicto?. En este caso es fácil, todo el texto estaría correcto, por lo que bastaría con borrar las líneas especiales para generar tener el fichero corregido. Cuando es código, podemos elegir una versión u otra o incluso generar código nuevo mezclando las dos versiones, si es necesario.

Después de realizar los cambios necesarios miro a ver cómo está el repositorio.

~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama y 'origin/prueba' han divergido,
y tienen 1 y 1 commits diferentes cada una respectivamente.
  (usa "git pull" para fusionar la rama remota en la tuya)

Todos los conflictos resueltos pero sigues fusionando.
  (usa "git commit" para concluir la fusion)

Cambios a ser confirmados:

	modificado:     archivo2.txt

~/proyectos/proyecto-nuevo>

Pues termino la fusión.

~/proyectos/proyecto-nuevo> git commit
[prueba 8753c00] Merge branch 'prueba' of /home/notxor/nube/proyecto-nuevo into prueba
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama está adelantada a 'origin/prueba' por 2 confirmaciones.
  (usa "git push" para publicar tus confirmaciones locales)

nada para hacer comit, el arbol de trabajo esta limpio
~/proyectos/proyecto-nuevo> git push
Contando objetos: 6, listo.
Delta compression using up to 4 threads.
Comprimiendo objetos: 100% (6/6), listo.
Escribiendo objetos: 100% (6/6), 659 bytes | 659.00 KiB/s, listo.
Total 6 (delta 3), reused 0 (delta 0)
To /path/a-la/nube/proyecto-nuevo.git
   bf825fc..8753c00  prueba -> prueba
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama está actualizada con 'origin/prueba'.

nada para hacer comit, el arbol de trabajo esta limpio

Bien. Ya está solucionado el conflicto.

*   8753c00 (HEAD -> prueba, origin/prueba) Merge branch 'prueba' of /home/notxor/nube/proyecto-nuevo into prueba
|\  
| * bf825fc Añadido desde otro ordenador.
* | 8f8437c Cambios en local pretendiendo que haya conflictos.
|/  
* a60c91a (origin/master, master) Añadir el archivo2.txt a la rama de pruebas.
* ee63815 Añadir el archivo1.txt a la rama de pruebas.
* f1f91e9 Primer commit del repositorio añadiendo el README.md.

Así nos queda el repositorio después de los cambios.

Revertir cambios

Hay ocasiones en las que acabo de subir un cambio y me doy cuenta de que falta algo o habría que corregir algún tema. Hay dos opciones. La primera es hacer un nuevo commit arreglando el entuerto o, la segunda, revertir los cambios.

~/proyectos/proyecto-nuevo> echo "Añadir nueva línea para revertir cambios." >> archivo2.txt 
notxor@oldulo:~/proyectos/proyecto-nuevo> cat archivo2.txt 
Esto es el fichero de prueba 2.
Cambios en el repositorio local.
Esto es una línea creada por otro usuario.
Añadir nueva línea para revertir cambios.
~/proyectos/proyecto-nuevo> git add archivo2.txt 
~/proyectos/proyecto-nuevo> git commit -m "Añadir cambios para revertirlos después."
[prueba b1120fe] Añadir cambios para revertirlos después.
 1 file changed, 1 insertion(+)

He añadido un commit a la línea del repositorio, pero quiero volver atrás. Si es el último commit es sencillo puedo utilizar la opción --amend del comando commit o puedo utilizar el comando revert.

~/proyectos/proyecto-nuevo> git commit --amend
[prueba 4a0256f] Añadir cambios para revertirlos después.
 Date: Wed Nov 15 10:05:17 2017 +0100
 1 file changed, 1 insertion(+)

Nuestro repositorio a estas alturas tendrá la siguiente forma:

* 4a0256f (HEAD -> prueba) Añadir cambios para revertirlos después.
*   8753c00 (origin/prueba) Merge branch 'prueba' of /home/notxor/nube/proyecto-nuevo into prueba
|\  
| * bf825fc Añadido desde otro ordenador.
* | 8f8437c Cambios en local pretendiendo que haya conflictos.
|/  
* a60c91a (origin/master, master) Añadir el archivo2.txt a la rama de pruebas.
* ee63815 Añadir el archivo1.txt a la rama de pruebas.
* f1f91e9 Primer commit del repositorio añadiendo el README.md.

Bien, vale, no se ve que haya borrado nada. ¿Cómo borro el commit para hacer como si nunca hubiera existido?

Para eso utilizo el comando reset bien con una opción --soft si no quiero perder los cambios locales o con una opción --hard si quiero quedarme en el mismo estado que estaba el commit al que quiero ir.

~/proyectos/proyecto-nuevo> git reset 8753c00 --soft
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama está actualizada con 'origin/prueba'.

Cambios a ser confirmados:
  (usa "git reset HEAD <archivo>..." para sacar del area de stage)

	modificado:     archivo2.txt

~/proyectos/proyecto-nuevo>

En el caso del reset --soft conservo los cambios que hice en el repositorio local.

~/proyectos/proyecto-nuevo> git reset 8753c00 --hard
HEAD está ahora en 8753c00 Merge branch 'prueba' of /home/notxor/nube/proyecto-nuevo into prueba
~/proyectos/proyecto-nuevo> git status
En la rama prueba
Tu rama está actualizada con 'origin/prueba'.

nada para hacer comit, el arbol de trabajo esta limpio

En este caso, desaparecen todos los cambios que haya hecho en el repositorio local y lo deja en el estado en el que estaba en el commit marcado como 8753c00.

Borrar rama

Algunas veces utilizo una rama sólo para hacer unas pruebas y hechas esas pruebas según el resultado haremos un merge o no, con la rama principal. El caso es que cuando ya no me interesa utilizar esa rama la quiero borrar.

Primero me voy a la rama principal, no se puede borrar una rama cuando la tiene activa (como si intentas cortar una rama de un árbol, en la que estás sentado).

~/proyectos/proyecto-nuevo> git checkout master 
Cambiado a rama 'master'
Tu rama está actualizada con 'origin/master'.
~/proyectos/proyecto-nuevo> git branch -d prueba 
warning: borrando la rama 'prueba' que ha sido fusionada en
	 'refs/remotes/origin/prueba', pero aún no ha sido fusionada a HEAD.
Eliminada la rama prueba (era 8753c00)..

La rama ya la fusionamos... ahora vamos a colocar nuestro HEAD en el sitio correcto.

~/proyectos/proyecto-nuevo> git reset 8753c00 --hard
HEAD está ahora en 8753c00 Merge branch 'prueba' of /path/a-la/nube/proyecto-nuevo into prueba
~/proyectos/proyecto-nuevo> git push
Total 0 (delta 0), reused 0 (delta 0)
To /path/a-la/nube/proyecto-nuevo.git
   a60c91a..8753c00  master -> master

Ahora mismo tenemos todos los diales apuntando al mismo lugar o commit. Lo podemos ver haciendo un log.

~/proyectos/proyecto-nuevo> git log --oneline --graph --all
*   8753c00 (HEAD -> master, origin/prueba, origin/master) Merge branch 'prueba' of /home/notxor/nube/proyecto-nuevo into prueba
|\  
| * bf825fc Añadido desde otro ordenador.
* | 8f8437c Cambios en local pretendiendo que haya conflictos.
|/  
* a60c91a Añadir el archivo2.txt a la rama de pruebas.
* ee63815 Añadir el archivo1.txt a la rama de pruebas.
* f1f91e9 Primer commit del repositorio añadiendo el README.md.

¿Cómo? Sigue habiendo una rama origin/prueba... ¿no la había borrado? Bueno, sí y no. Había borrado la rama local pero la remota aún no la he tocado. Falta hacer un push para normalizar las ramas en el remoto también.

~/proyectos/proyecto-nuevo> git push origin --delete prueba
To /path/a-la/nube/proyecto-nuevo.git
 - [deleted]         prueba

Conclusión

git es una herramienta con una potencia que apenas he empezado a rascar superficialmente y merece la pena el esfuerzo en aprender sus modos y maneras. Venir de otros sistemas de control de versiones hicieron que me costara al principio. Algunos comandos coinciden en nombre, pero su función es distinta.

Estos dos artículos son en realidad el pasar a limpio mis apuntes sobre git y cómo he entendido yo las cosas. Quiero decir, que puedo estar equivocado y que algo no sea exactamente como cuento en ellos. Y que iré actualizando el contenido, para corregirlo o para ampliarlo si es necesario.

De las cosas que hablo en los dos artículos, la mayoría se pueden hacer con el comando gitk --all que muestra un entorno gráfico tk con el estado de nuestro repositorio y con el gui (que es otro entorno gráfico tk) que nos auxilia en las acciones de actualizar la información entre los repositorios. Sin embargo, me parece importante saber qué estás haciendo en cada momento y cómo funciona la herramienta que hay por debajo.


Comentarios