(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!