(NOTA: Los primeros "truquillos Git" fueron redactados antes de que en Git la rama por defecto pasase a ser main, y he usado --initial-branch=master como manera fácil de adaptar los ejemplos para que siguiesen funcionado. Los "truquillos" posteriores ya usan main.)
Ejecutar este script en Git Bash crea un repositorio y lo deja en un estado de conflicto de merge:
mkdir conflicto.01
cd conflicto.01
git init --initial-branch=master
echo "zzz" > foo.txt
printf "aaa\nbbb\nccc" > bar.txt
git add .
git commit -m "first commit"
git checkout -b rama
sed -i -s 's/bbb/zzz/' bar.txt
git commit --all -m "bar modificado en rama"
git checkout master
git rm bar.txt
git commit --all -m "bar borrado en master"
git merge rama
echo "Ready!"
En concreto, el conflicto es el siguiente:
CONFLICT (modify/delete): bar.txt deleted in HEAD and modified in rama. Version rama of bar.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.
O sea, hemos borrado bar.txt en master, pero en rama lo habíamos
modificado. Esto es un conflicto porque las líneas que han cambiado en rama pueden ser
importantes.
Utilizando la línea de comandos de Git, cómo podemos averiguar cuáles fueron
las líneas cambiadas en rama?
En nuestro working tree, tenemos la versión de bar.txt proveniente de rama:
$ cat bar.txt
aaa
zzz
ccc
Pero esto no resulta demasiado útil, porque en situaciones reales el archivo puede ser grande y los cambios no van a ser obvios. Necesitamos algún tipo de diff explícito!
El comando git log tiene la opción --patch que lista las diferencias introducidas por una serie de commits.
También tiene una opción --merge especialmente pensada para conflictos de merge. Según la documentación:
--merge
After a failed merge, show refs that touch files having a conflict and don’t exist on all heads to merge.
--merge lista commits que se refieran a archivos que estén en conflicto y que además estén ausentes en una de las ramas del merge.
Combinando ambas opciones:
$ git log --patch --merge
commit d9563ffcc0d35c632b38cb0f1211c141aabcccfa (HEAD -> master)
Author:
Date:
bar borrado en master
diff --git a/bar.txt b/bar.txt
deleted file mode 100644
index 8cc5896..0000000
--- a/bar.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-aaa
-bbb
-ccc
\ No newline at end of file
commit aec567a05c768d86c27bca2e4ff763d12a616807 (rama)
Author:
Date:
bar modificado en rama
diff --git a/bar.txt b/bar.txt
index 8cc5896..6536f0e 100644
--- a/bar.txt
+++ b/bar.txt
@@ -1,3 +1,3 @@
aaa
-bbb
+zzz
ccc
\ No newline at end of file
Ahora ya tenemos un diff explícito que nos dice que la línea zzz fue añadida en el commit aec567a.
Si hubiese habido más de un archivo en conflicto, podríamos habernos quedado exclusivamente con los commits que afectasen a bar.txt:
git log --patch --merge -- bar.txt
El -- aislado le dice a Git que lo que viene a continuación es el path de un archivo. No siempre es necesario, pero a veces hace falta para desambiguar entre nombres de archivo y ramas.
Queda otro posible refinamiento. El log nos mostró el commit d9563ff que fue el que borró el archivo en HEAD. Pero en realidad no nos interesaba; ya sabíamos que el archivo se había borrado en HEAD. Ese commit es ruido.
Podemos usar la sintaxis ^HEAD, que le va a decir a git log que excluya de su salida todos los commits que sean HEAD o ancestros de HEAD:
$ git log --patch --merge ^HEAD -- bar.txt
commit aec567a05c768d86c27bca2e4ff763d12a616807 (rama)
Author:
Date:
bar modificado en rama
diff --git a/bar.txt b/bar.txt
index 8cc5896..6536f0e 100644
--- a/bar.txt
+++ b/bar.txt
@@ -1,3 +1,3 @@
aaa
-bbb
+zzz
ccc
\ No newline at end of file
El commit que añade las líneas no se ve afectado porque reside en otra rama.
La sintaxis que hemos usado para excluir commits está explicada aquí.
También podemos recurrir directamente a git diff.
En git diff, la notación de rango <commit>...<commit> significa: "encuentra el ancestro común de los dos commits, y muéstrame las diferencias entre dicho ancestro y el segundo commit".
Que es justo lo que necesitamos; mostrar los cambios en rama desde el punto en el cual HEAD y rama divergieron:
$ git diff HEAD...rama -- bar.txt
diff --git a/bar.txt b/bar.txt
index 8cc5896..6536f0e 100644
--- a/bar.txt
+++ b/bar.txt
@@ -1,3 +1,3 @@
aaa
-bbb
+zzz
ccc
\ No newline at end of file
Vuelve a resultar evidente que fue añadida la línea zzz.
Otras maneras equivalentes de invocar el diff serían:
$ git diff ...rama -- bar.txt # Aquí HEAD se asume por defecto
$ git diff ...MERGE_HEAD -- bar.txt # La referencia MERGE_HEAD apunta a la rama que estamos mergeando
En cambio comparar directamente HEAD y MERGE_HEAD usando .. en vez de ... no nos habría servido de nada, porque todas las líneas de bar.txt se mostrarían como añadidas:
$ git diff HEAD..MERGE_HEAD -- bar.txt
diff --git a/bar.txt b/bar.txt
new file mode 100644
index 0000000..6536f0e
--- /dev/null
+++ b/bar.txt
@@ -0,0 +1,3 @@
+aaa
+zzz
+ccc
\ No newline at end of file
Nota: La notación de rango de commits de dos puntos .. y también la de tres puntos ... hacen cosas diferentes en git diff y en git log, como explica la documentación:
"diff" is about comparing two endpoints, not ranges, and the range
notations ("<commit>..<commit>" and "<commit>...<commit>") do not mean a
range as defined in the "SPECIFYING RANGES" section in gitrevisions[7].
Lo cual me parece muy confuso, la verdad.
El plugin de Git para IntelliJ es lo bastante listo para mostrar las líneas cambiadas de bah.txt en rama mientras resolvemos el merge.
No he sido capaz de configurar lo mismo en Sourcetree. Si alguien sabe cómo hacerlo, agradecería que lo explicase en un comentario!