tab - ¿Cómo detectas una fusión malvada en git?
git tags best practices (7)
¿Qué hay de rehacer la fusión ''virtualmente'' y comparar el resultado? En otras palabras
pseudo codigo
- empezando por yo
- obtener los 2 padres: G, H
-
git checkout E
-
git merge H
- ahora tienes nuevo-yo.
- compare I y new-I, ya sea utilizando
git diff
o comparando la salida degit show I
ygit show new-I
Especialmente el último paso será un trabajo duro, si desea hacerlo de forma completamente automática, al menos si hubo conflictos en la confirmación.
He creado un repositorio git simple para ilustrar mi pregunta, disponible en GitHub aquí: https://github.com/smileyborg/EvilMerge
Aquí hay una ilustración de la historia repo:
master A---B---D---E-----G-----I
/ / / /
another_branch ----C / /
/ /
another_branch2 F---H
(En el repositorio real en GitHub, D
es 4a48c9
, y I
es 48349d
.)
D
es una combinación malvada "simple", en la que la confirmación de fusión "correctamente" resuelve un conflicto de combinación, pero también realiza un cambio "maligno" no relacionado que no existía en ninguno de los padres. Es posible descubrir la parte "malvada" de esta combinación utilizando git show -c
en este commit, ya que la salida incluye ++
y --
(a diferencia de single +
y -
) para indicar los cambios que no existían en cualquiera de los padres (ver esta respuesta para el contexto).
I
es un tipo diferente de combinación malvada, donde la confirmación de fusión "correctamente" resuelve un conflicto de fusión (causado por los cambios de F
a file.txt
que entran en conflicto con los cambios de G
), pero también "maliciosamente" descarta los cambios realizados completamente. archivo diferente file2.txt
(deshaciendo efectivamente los cambios de H
).
¿Cómo puedes saber que I
una fusión maligna? En otras palabras, ¿qué comando (s) puede usar para descubrir que no solo resuelvo un conflicto manualmente, sino que también falla en fusionar los cambios que debería tener?
Editar / Actualizar: ¿Qué es una fusión malvada?
Como señala René Link a continuación, es difícil (tal vez imposible) definir un conjunto genérico de criterios para identificar una "fusión maligna". Sin embargo, al igual que el juez de la Corte Suprema Stewart dijo acerca de la pornografía , las combinaciones malvadas son algo que uno sabe cuando ve.
Entonces, tal vez una mejor pregunta que hacer es la siguiente: ¿qué comando (s) git puede usar en una confirmación de combinación para obtener una salida de diferencias de todos los cambios nuevos introducidos únicamente en la propia confirmación de combinación ? Esta diferencia debe incluir:
- todas combinan resoluciones de conflicto (al menos, si la resolución involucró algo más complejo que elegir los cambios de un padre sobre los del otro)
- todas las adiciones o eliminaciones que no existían en ninguno de los padres (como se ve en
D
) - todos los cambios que existieron en uno de los padres pero que la fusión comete descartes (como se ve en
I
)
El objetivo aquí es poder tener una visión humana de esta salida y saber si la fusión fue exitosa o (maliciosa o accidentalmente) " sin ", sin tener que volver a revisar todos los cambios revisados previamente (por ejemplo, F
y H
) que Se están integrando en la fusión.
Antes de que podamos detectar las fusiones malignas, debemos definir qué son fusiones malvadas.
Cada fusión que tenga conflictos debe ser resuelta manualmente. Para resolver conflictos podemos
- toma uno de los cambios y omite el otro.
- finalmente tome ambos cambios (en este caso, el orden en el resultado puede ser importante)
- no tome ninguno de ellos y cree un nuevo cambio que sea la consolidación de ambos.
- no tomes ninguno de ellos y omite ambos.
Entonces, ¿qué es una fusión del mal ahora?
Según este blog es
una fusión se considera malvada si no integra fielmente todos los cambios de todos los padres.
Entonces, ¿qué es una "integración fiel"? Creo que nadie puede dar una respuesta general, ya que depende de la semántica del código o texto o de lo que se fusione.
Una combinación malvada es una combinación que introduce cambios que no aparecen en ningún padre.
Con esta definición todos los conflictos que se resuelven mediante
- toma uno de los cambios y omite el otro.
- no tome ninguno de ellos y cree un nuevo cambio que sea la consolidación de ambos.
- no tomes ninguno de ellos y omite ambos.
son las fusiones malvadas.
Así que finalmente llegamos a las preguntas.
Es legal
- ¿Solo tomar uno de los cambios y omitir el otro?
- tomar los dos cambios?
- ¿No tomas ninguno de ellos y creas un nuevo cambio que es la consolidación de ambos?
- no tomar ninguno de ellos y omitir ambos?
Y las cosas pueden volverse más complejas si pensamos en la fusión de pulpos.
Mi conclusión
La única fusión perversa que podemos detectar es una fusión que se realizó sin conflictos. En este caso, podemos rehacer la fusión y compararla con la fusión que ya se realizó. Si hay diferencias que alguien introdujo más de lo que debería, podemos estar seguros de que esta fusión es una fusión maligna.
Al menos creo que debemos detectar las fusiones malvadas manualmente, porque depende de la semántica de los cambios y no puedo formular una definición formal de lo que es una fusión malvada.
He ampliado la respuesta de Joseph K. Strauss para crear dos scripts de shell completos que se pueden usar fácilmente para obtener un resultado de diferencia significativo para un compromiso de fusión dado.
Los scripts están disponibles en este GitHub Gist: https://gist.github.com/smileyborg/913fe3221edfad996f06
La primera secuencia de comandos, detect_evil_merge.sh
, utiliza la estrategia de rehacer automáticamente la fusión de nuevo sin resolver ningún conflicto, y luego la difunde a la fusión real.
La segunda secuencia de comandos, detect_evil_merge2.sh
, utiliza la estrategia de rehacer la fusión automáticamente dos veces, una vez que resuelve los conflictos con la primera versión del padre y la segunda resolución de los conflictos con la segunda versión del padre, y luego difumina cada uno de ellos a la fusión real .
Cualquiera de los dos scripts hará el trabajo, es solo una preferencia personal en la forma en que le resulte más fácil entender cómo se resolvieron los conflictos.
Lo más fácil sería difuminar los resultados de su resolución de conflictos con una combinación que resuelva los conflictos automáticamente sin intervención humana. Cualquier resolución automática será ignorada, ya que serán resueltas exactamente de la misma manera.
Veo dos formas de visualizar las posibles resoluciones "malvadas". Si está convirtiendo esto en un script, agregue &> /dev/null
al final de todas las líneas que no le interesa ver el resultado.
1) Use dos diferencias separadas, una que favorezca al primer padre y una segunda que favorezca al segundo padre.
MERGE_COMMIT=<Merge Commit>
git checkout $MERGE_COMMIT~
git merge --no-ff --no-edit -s recursive -Xours $MERGE_COMMIT^2
echo "Favor ours"
git diff HEAD..$MERGE_COMMIT
git checkout $MERGE_COMMIT~
git merge --no-ff --no-edit -s recursive -Xtheirs $MERGE_COMMIT^2
echo "Favor theirs"
git diff HEAD..$MERGE_COMMIT
2) Diff contra los resultados de la fusión conflictiva con los conflictos aún en.
MERGE_COMMIT=<Merge Commit>
git checkout $MERGE_COMMIT~
git -c merge.conflictstyle=diff3 merge --no-ff $MERGE_COMMIT^2 --no-commit
git add $(git status -s | cut -c 3-)
git commit --no-edit
git diff HEAD..$MERGE_COMMIT
Lo más simple es probablemente lo mejor aquí: difunda los resultados de una combinación automática no corregida (e incompleta) desechable, sin que se hayan resuelto los conflictos, si existen, con los resultados reales de la fusión.
Las resoluciones ordinarias de nuestro / ellos aparecerán como todas las 3 (4 para un 3diff) líneas de marcador de conflicto eliminadas, y un lado u otro de los trozos de cambio también eliminados, que será fácil de hacer con el globo ocular.
Cualquier alteración de los cambios de cualquiera de las sucursales se mostrará como una mezcla de aspecto extraño, por ejemplo, cualquier tío que se haya agregado o eliminado de forma gratuita se mostrará fuera de los marcadores de conflicto.
En el ejemplo repo, después de
git clone https://github.com/smileyborg/EvilMerge
git checkout master^
git merge --no-commit master^2 # --no-commit so w/ or w/o conflict work the same
corriendo el diff sugerido obtiene
$ git diff -R master # -R so anything master adds shows up as an add
diff --git b/file.txt a/file.txt
index 3835aac..9851407 100644
--- b/file.txt
+++ a/file.txt
@@ -1,12 +1,6 @@
This is a file in a git repo used to demonstrate an ''evil merge''.
-<<<<<<< HEAD
-int a = 3;
-||||||| merged common ancestors
-int a = 1;
-=======
-int d = 1;
->>>>>>> master^2
+int d = 3;
int b = 0;
int c = 2;
b = a;
diff --git b/file2.txt a/file2.txt
index d187a25..538e79f 100644
--- b/file2.txt
+++ a/file2.txt
@@ -4,6 +4,6 @@ int x = 0;
int y = 1;
int z = 2;
x = y;
-x--;
-y--;
-z--;
+x++;
+y++;
+z++;
y al instante queda claro que algo file2.txt
: en file.txt
los cambios en ambas ramas se descartaron y se insertó una línea de la nada, mientras que en file2.txt
nunca hubo un conflicto, y la combinación simplemente cambia el código de forma gratuita. Una pequeña investigación muestra que hay una reversión de compromiso aquí, pero eso no tiene importancia, el punto es que los cambios habituales siguen patrones reconocibles y cualquier cosa inusual es fácilmente detectable y vale la pena verificarla.
Del mismo modo, después de
git branch -f wip 4a48
git checkout wip^
git merge --no-commit wip^2
corriendo el diff sugerido obtiene
$ git diff -R wip
diff --git b/file.txt a/file.txt
index 3e0e047..fe5c38a 100644
--- b/file.txt
+++ a/file.txt
@@ -1,19 +1,9 @@
This is a file in a git repo used to demonstrate an ''evil merge''.
-<<<<<<< HEAD
-int a = 0;
-int b = 1;
-int c = 2;
-a = b;
-||||||| merged common ancestors
-int a = 0;
-int b = 1;
-a = b;
-=======
int a = 1;
int b = 0;
+int c = 2;
b = a;
->>>>>>> wip^2
a++;
-b++;
+b--;
c++;
y nuevamente la rareza salta: wip agregó un int c = 2
a los cambios de la rama wip^2
, y b--
a b++
de la nada.
A partir de aquí, podría ponerse lindo y automatizar algunas de las cosas predecibles para hacer que la investigación a granel vaya más rápido, pero esa es realmente una pregunta aparte.
Nota preliminar: Estoy usando la definition de Linus Torvalds de una "Combinación al Mal" aquí, que como notes Junio Hamano a veces puede ser algo bueno (por ejemplo, para resolver conflictos semánticos en lugar de conflictos textuales). Aquí está la definición de Linus:
una "fusión malvada" es algo que hace cambios que provienen de ninguno de los dos lados y en realidad no resuelven un conflicto [Fuente: definition
Como señaló en su respuesta , el problema con cualquier detección de fusión malvada basada únicamente en "-c" o "--cc" es este:
"Además, enumera solo archivos que fueron modificados de todos los padres". [Fuente: man git-log]
Y para detectar la maldad de I , necesitamos encontrar archivos modificados por algunos, pero no todos , de sus padres.
Creo que las fusiones limpias tienen una propiedad simétrica. Considera este diagrama:
En una fusión limpia, las diagonales son las mismas: b1 == m2 y b2 == m1 . Los conjuntos de líneas modificadas solo se superponen cuando se producen conflictos y las combinaciones limpias no tienen conflictos. Y así, el conjunto de cambios en b2 debe coincidir con m1 , ya que todo el punto de b2 es reproducir m1 en la parte superior de parent2, para sincronizar parent2 con parent1 (y recuerde que --- no hubo conflictos). Y viceversa para m2 y b1 .
Otra forma de pensar acerca de esta simetría: cuando cambiamos de base, básicamente desechamos b1 y lo reemplazamos con m2 en su lugar.
Y así, si desea detectar combinaciones malvadas, puede usar "git show -c" para los archivos cambiados por ambos padres, y de lo contrario, verificar que la simetría sea válida para los cuatro segmentos del diagrama usando "git diff --name-only".
Si asumimos que la combinación del diagrama es HEAD (por ejemplo, veamos si la combinación que acabo de confirmar es mala), y usamos la elegante notación de git diff de "tres puntos" que calcula la fusión de la base para usted, creo que solo necesita estos cuatro lineas:
git diff --name-only HEAD^2...HEAD^1 > m1
git diff --name-only HEAD^1...HEAD^2 > b1
git diff --name-only HEAD^1..HEAD > m2
git diff --name-only HEAD^2..HEAD > b2
Luego analiza los contenidos para ver que m1 == b2 y b1 == m2 . Si no coinciden, entonces tienes el mal!
Cualquier salida de cualquiera de estos indica mal, ya que si catamos b1 y m2 y los ordenamos, entonces cada línea debería aparecer dos veces.
cat b1 m2 | sort | uniq -c | grep -v '' 2 ''
cat b2 m1 | sort | uniq -c | grep -v '' 2 ''
Y para el ejemplo de EvilMerge, commit I produce lo siguiente:
cat b2 m1 | sort | uniq -c | grep -v '' 2 ''
1 file2.txt
La edición a "file2.txt" solo ocurrió una vez entre las diagonales b2 y m1 . La combinación no es simétrica, por lo tanto, no es una combinación limpia. ¡MAL SUCESO DETECTADO!
Descargo de responsabilidad: como lo señaló @smileyborg, esta solución no detectará un caso en el que la fusión malvada haya revertido por completo el cambio que introdujo uno de los padres. Este defecto ocurre porque de acuerdo con los documentos de Git para la opción -c
Además, enumera solo archivos que fueron modificados de todos los padres.
Recientemente descubrí una solución mucho más simple para esta pregunta que cualquiera de las respuestas actuales.
Básicamente, el comportamiento predeterminado de git show
para combinaciones de mezcla debería resolver su problema. En los casos en que las modificaciones de ambos lados de la fusión no se toquen, y no se hayan realizado cambios "malos", no habrá salida dif. Anteriormente había pensado que git show
nunca muestra diferencias para las combinaciones de combinaciones. Sin embargo, si un compromiso de fusión implica un conflicto desordenado o una combinación malvada, entonces se mostrará un diff en formato combinado .
Para ver el formato combinado al ver un número de parches de confirmación con log -p
, simplemente agregue el parámetro --cc
.
En el ejemplo dado por GitHub en la pregunta, se muestra lo siguiente (con mis comentarios intercalados):
$ git show 4a48c9
( D en el ejemplo)
commit 4a48c9d0bbb4da5fb30e1d24ae4e0a4934eabb8d
Merge: 0fbc6bb 086c3e8
Author: Tyler Fox <[email protected]>
Date: Sun Dec 28 18:46:08 2014 -0800
Merge branch ''another_branch''
Conflicts:
file.txt
diff --cc file.txt
index 8be441d,f700ccd..fe5c38a
--- a/file.txt
+++ b/file.txt
@@@ -1,9 -1,7 +1,9 @@@
This is a file in a git repo used to demonstrate an ''evil merge''.
Las siguientes líneas no son malas. Los cambios realizados por el primer padre se indican con un +
/ -
en la columna de la izquierda; los cambios realizados por el segundo padre se indican con +
/ -
en la segunda columna.
- int a = 0;
- int b = 1;
+ int a = 1;
+ int b = 0;
+int c = 2;
- a = b;
+ b = a;
a++;
Aquí está la parte mala: ++
se cambió a --
de ambos padres. Tenga en cuenta el encabezado --
y ++
que indica que estos cambios se producen de ambos padres, lo que significa que alguien introdujo nuevos cambios en este compromiso que aún no estaban reflejados en uno de los padres. No confunda el ++
/ --
inicial generado por diff con el ++
/ --
final --
que forma parte del contenido del archivo.
--b++;
++b-- ;
Fin de la maldad.
+c++;
Para ver rápidamente todos los compromisos de fusión que pueden tener problemas:
git log --oneline --min-parents=2 --cc -p --unified=0
Todas las combinaciones no interesantes se mostrarán en una sola línea, mientras que las desordenadas (malvadas o de otro tipo) mostrarán la diferencia combinada.
Explicación:
-
-p
- Parche de pantalla -
--oneline
- Muestra cada encabezado de confirmación en una sola línea -
--min-parents=2
- Mostrar solo--min-parents=2
. -
--cc
: muestra la combinación de diferencias, pero solo para lugares donde los cambios de ambos padres se superponen -
--unified=0
- Muestra 0 líneas de contexto; Modifica el número para que sea más agresivo en la búsqueda de fusiones malignas.
Alternativamente, agregue lo siguiente para eliminar todas las confirmaciones no interesantes:
-z --color=always | perl -pe ''s/^([^/0]*/0/0)*([^/0]*/0/0)(.*)$//n$2/n$3/''
-
-z
- Muestra NUL en lugar de nueva línea al final de los registros de confirmación -
--color=always
- No desactive el color cuando se canaliza a perl -
perl -pe ''s/^([^/0]*/0/0)*([^/0]*/0/0)
- Da masajes a la salida para ocultar las entradas del registro con diffs vacíos