Как вы обнаруживаете злое слияние в git?
Я создал простой репозиторий git, чтобы проиллюстрировать мой вопрос, доступный на GitHub здесь: https://github.com/smileyborg/EvilMerge
Здесь показана история репо:
master A---B---D---E-----G-----I
\ / \ /
another_branch ----C \ /
\ /
another_branch2 F---H
(В реальном репо на GitHub D
есть 4a48c9
, а I
- 48349d
.)
D
- это "простое" объединение зла, где слияние "правильно" разрешает конфликт слияния, но также делает несвязанное "зло" изменение, которого не было ни в одном из родителей. Можно обнаружить "злую" часть этого слияния, используя git show -c
для этого фиксации, поскольку на выходе есть ++
и --
(в отличие от одиночных +
и -
), чтобы указать изменения которые не существовали ни в одном из родителей (см. этот ответ для контекста).
I
- это другой вид слияния зла, где слияние "правильно" разрешает конфликт слияния (вызванный изменениями от F
до file.txt
, которые конфликтуют с изменениями из G
), но также "злобно" отбрасывает изменения, внесенные в совершенно другой файл file2.txt
(эффективно отменяя изменения от H
).
Как вы знаете, что I
- это злое слияние? Другими словами, какие команды вы можете использовать для обнаружения того, что I
не только вручную разрешает конфликт, но также не может слить изменения, которые он должен иметь?
Изменить/обновить: что такое слияние зла?
Как указано René Link ниже, трудно (возможно, невозможно) определить общий набор критериев для определения "злого слияния". Однако, как Судья Верховного суда Стюарт сказал о порнографии, дурные слияния являются чем-то вы знаете, когда вы видите.
Поэтому, возможно, лучше задать вопрос: какие команды git вы можете использовать при компиляции слияния, чтобы получить diff-выход всех новых изменений, введенных исключительно в самом делете слияния. Этот diff должен включать:
- все слияния разрешений конфликтов (по крайней мере, если разрешение включало в себя что-то более сложное, чем выбор одного родителя, изменяющегося по сравнению с другим)
- все добавления или удаления, которые не существовали ни в одном из родителей (как показано в
D
)
- все изменения, которые существовали у одного из родителей, но сброс слияния (как показано в
I
)
Цель состоит в том, чтобы иметь возможность взглянуть на этот результат на человека и знать, было ли слияние успешным или (случайно или злонамеренно) "злым" без, чтобы пересмотреть все ранее (например, F
и H
), которые интегрируются в слияние.
Ответы
Ответ 1
Я расширил ответ от Джозефа К. Штрауса, чтобы создать два полных сценария оболочки, которые могут быть легко использованы для получения значимого результата diff для данного слияния совершить.
Сценарии доступны в этом GitHub Gist: https://gist.github.com/smileyborg/913fe3221edfad996f06
Первый script, detect_evil_merge.sh
использует стратегию автоматического повторного слияния слияния без разрешения конфликтов, а затем отличается от фактического слияния.
Второй script, detect_evil_merge2.sh
использует стратегию автоматического повторного слияния слияния дважды, после разрешения конфликтов с первой родительской версией и второго разрешения конфликтов с использованием второй родительской версии, а затем разграничения каждой из для фактического слияния.
Либо script выполнит эту работу, это просто личное предпочтение, и вам будет легче понять, как были разрешены конфликты.
Ответ 2
Проще всего сделать, чтобы сравнить результаты вашего разрешения конфликта с слиянием, которое автоматически разрешает конфликты без вмешательства человека. Любые автоматические разрешения будут игнорироваться, так как они будут разрешены точно так же.
Я вижу два способа визуализации возможных "злых" резолюций. Если вы делаете это в script добавьте &> /dev/null
в конец всех строк, которые вам не нравятся.
1) Используйте два отдельных отличия, один из которых соответствует первому родительскому элементу, а второй - второму родительскому.
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) Повреждение результатов конфликтуемого слияния с конфликтами, которые еще находятся.
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
Ответ 3
Прежде чем мы сможем обнаружить злые слияния, мы должны определить, что такое слияние зла.
Каждое слияние с конфликтами должно быть разрешено вручную.
Чтобы разрешить конфликты, мы можем
- выполнить одно из изменений и опустить другое.
- в конечном итоге принять оба изменения (в этом случае порядок в результате может быть важен)
- не принимать ни одного из них и создавать новое изменение, которое является объединением обоих.
- не брать ни одного из них и опустить оба.
Итак, что же представляет собой злое слияние?
Согласно этот блог,
слитие считается злым, если оно не верно интегрирует все изменения со всех родителей.
Итак, что такое "верная интеграция"? Я думаю, никто не может дать общий ответ, потому что это зависит от семантики кода или текста или от того, что сливается.
Другие говорят
Слияние зла - это слияние, которое вводит изменения, которые не отображаются ни в одном родителе.
С этим определением все конфликты, разрешенные
- выполнить одно из изменений и опустить другое.
- не принимать ни одного из них и создавать новое изменение, которое является объединением обоих.
- не брать ни одного из них и опустить оба.
- злые слияния.
Итак, мы, наконец, дошли до вопросов.
Является ли это законным для
- выполнить одно из изменений и опустить другое?
- взять оба изменения?
- не принимать ни одного из них и создавать новое изменение, которое является консолидацией обоих?
- не брать ни одного из них и опустить оба?
И все может усложниться, если мы подумаем о слиянии осьминогов.
Мой вывод
Единственное злое слияние, которое мы можем обнаружить, - это слияние, которое было сделано без конфликтов. В этом случае мы можем повторить слияние и сравнить его с уже выполненным слиянием. Если есть различия, чем кто-то вводил больше, чем он/она, и мы можем быть уверены, что это слияние - это злое слияние.
По крайней мере, я думаю, что мы должны обнаруживать, что зло сливается вручную, потому что оно зависит от семантики изменений, и я не могу сформулировать формальное определение того, что такое слияние зла.
Ответ 4
Отказ от ответственности: Как указано в @smileyborg, это решение не обнаружит случая, когда злое слияние полностью вернуло изменение, внесенное одним из родителей. Этот дефект возникает, потому что согласно Git Документам для параметра -c
Кроме того, в нем перечислены только файлы, которые были изменены от всех родителей.
Недавно я обнаружил гораздо более простое решение этого вопроса, чем любой из текущих ответов.
В принципе, поведение по умолчанию git show
для коммандов merge должно решить вашу проблему. В тех случаях, когда модификации с обеих сторон слияния не касаются, и никаких "злых" изменений не было сделано, не будет никакого выхода diff. Раньше я думал, что git show
никогда не показывает diff для коммитов. Однако, если слияние слияния связано с беспорядочным конфликтом или слиянием зла, тогда diff будет отображаться в комбинированном формате.
Чтобы просмотреть комбинированный формат при просмотре нескольких патчей фиксации с помощью log -p
, просто добавьте параметр --cc
.
В примере из GitHub в вопросе отображается следующее (с моими комментариями вкраплены):
$ git show 4a48c9
( D в примере)
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'.
Следующие строки не являются злыми. Изменения, сделанные первым родителем, обозначаются символом +
/-
в самом левом столбце; изменения, внесенные вторым родителем, обозначаются символом +
/-
во втором столбце.
- int a = 0;
- int b = 1;
+ int a = 1;
+ int b = 0;
+int c = 2;
- a = b;
+ b = a;
a++;
Вот злая часть: ++
изменилось на --
от обоих родителей. Обратите внимание на ведущие --
и ++
, указывающие на то, что эти изменения происходят от обоих родителей, а это означает, что кто-то ввел новые изменения в этом фиксации, которые еще не были отражены в одном из родителей. Не путайте ведущий, diff-сгенерированный ++
/--
с завершающим ++
/--
, который является частью содержимого файла.
--b++;
++b-- ;
Конец зла.
+c++;
Чтобы быстро просмотреть все коммиты слияния, которые могут иметь проблемы:
git log --oneline --min-parents=2 --cc -p --unified=0
Все неинтересные слияния будут отображаться на одной строке, а грязные - злые или иначе - отобразятся объединенные различия.
Пояснение:
-
-p
- Показать патч
-
--oneline
- отображает каждый заголовок фиксации в одной строке
-
--min-parents=2
- показывать только слияния.
-
--cc
- Показать комбинированный diff, но только для мест, где изменения обоих родителей перекрываются.
-
--unified=0
- отображать 0 строк контекста; Измените число, чтобы быть более агрессивным в поиске слияний.
В качестве альтернативы добавьте следующее, чтобы устранить все неинтересные коммиты:
-z --color=always | perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0)(.*)$/\n$2\n$3/'
-
-z
- отображать NUL вместо новой строки в конце журналов фиксации
-
--color=always
- Не отключайте цвет при подключении к perl
-
perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0)
- Массируйте вывод, чтобы скрыть записи журнала с пустыми различиями
Ответ 5
Простейший, вероятно, лучше всего здесь: отбросить результаты некорректного (и неполного) автосекретаря без конфликтов, если они есть, с фактическими результатами слияния.
Обычные наши/их разрешения будут отображаться как все 3 (4 для 3diff) строки маркера конфликта удалены, а одна сторона или другая группа изменений также будет удалена, что будет легко для глазного яблока.
Любые изменения любых изменений в ветке будут отображаться как странный вид, например, любые добавленные или удаленные куски будут отображаться вне маркеров конфликта.
В примере репо после
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
запуск предлагаемого diff получает
$ 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++;
и он мгновенно очистит что-то подозрительное: в file.txt
изменения на обеих ветвях были отброшены и строка из ниоткуда вставлена., в то время как в file2.txt
конфликта никогда не было, а слияние просто безвозмездно меняет код. Небольшое копание показывает, что оно совершает реверсию здесь, но это несущественно, суть в том, что обычные изменения следуют легко узнаваемым паттернам, и что-то необычное легко обнаруживается и стоит проверить.
Аналогично, после
git branch -f wip 4a48
git checkout wip^
git merge --no-commit wip^2
запуск предлагаемого diff получает
$ 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++;
и снова вычет нечетности: wip добавил a int c = 2
в ветвь wip^2
, и он переключил b--
в b++
из ниоткуда.
Вы могли бы здесь получить симпатичный и автоматизировать некоторые предсказуемые вещи, чтобы сделать массовый опрос быстрее, но это действительно отдельный вопрос.
Ответ 6
Предварительное примечание: я использую Linus Torvalds определение "Злого слияния" здесь, которое, как Junio Hamano примечания иногда могут быть полезными (например, для разрешения семантических конфликтов, а не текстовых конфликтов). Вот определение Linus:
"злое слияние" - это то, что вносит изменения, которые не происходят ни с одной стороны, ни на самом деле не разрешают конфликт [Источник: LKML]
Как @joseph-k-strauss, отмеченный в его ответе, проблема с любым злом-слиянием обнаружение, основанное исключительно на "-c" или "--cc", таково:
"Кроме того, в нем перечислены только файлы, которые были изменены от всех родителей". [Источник: man git -log]
И чтобы обнаружить I злоумышленник, нам нужно найти файлы, измененные некоторыми, но не всеми, его родителями.
Я считаю, что чистые слияния имеют симметричное свойство. Рассмотрим эту диаграмму:
![введите описание изображения здесь]()
В чистом слиянии диагонали одинаковы: b1 == m2 и b2 == m1. Наборы измененных строк перекрываются только при возникновении конфликтов, а чистые слияния не имеют конфликтов. И поэтому набор изменений в b2 должен соответствовать m1, поскольку вся точка b2 должна переименовать m1 поверх родителя2, чтобы привести parent2 в синхронизацию с parent1 (и запомнить --- конфликтов не было). И наоборот для m2 и b1.
Еще один способ мышления об этой симметрии: когда мы перезаряжаем, мы по существу выбрасываем b1 и вместо этого заменяем его квадратом.
Итак, если вы хотите обнаружить слияние зла, вы можете использовать "git show -c" для файлов, измененных обоими родителями, и в противном случае проверить, что симметрия выполняется для четырех сегментов диаграммы, используя "git diff --name только".
Если мы предположим, что слияние с диаграммой будет HEAD (например, посмотрим, является ли только что слияние, которое я только что совершил, является злом), и мы используем фантастическую "трехточечную" условную обозначение git, которая вычисляет базу слияния для вас, я думайте, что вам нужны только эти четыре строки:
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
Затем проанализируйте содержимое, чтобы увидеть, что m1 == b2 и b1 == m2. Если они не совпадают, тогда у вас есть зло!
Любой вывод из любого из них указывает на зло, поскольку, если мы cat b1 и m2 и сортируем их, тогда каждая строка должна встречаться дважды.
cat b1 m2 | sort | uniq -c | grep -v ' 2 '
cat b2 m1 | sort | uniq -c | grep -v ' 2 '
И для примера EvilMerge commit I выводит следующее:
cat b2 m1 | sort | uniq -c | grep -v ' 2 '
1 file2.txt
Редактирование файла "file2.txt" происходит только один раз между диагоналями b2 и m1. Слияние не симметрично, поэтому это не чистое слияние. ЗЛО УСПЕШНО ОБНАРУЖЕНЫ!
Ответ 7
Как насчет пересоединения "виртуального" и сравнения результата? Другими словами
псевдокод:
- начиная с I
- получите 2 родителя: G, H
-
git checkout E
-
git merge H
- теперь у вас есть новый-я.
- сравнить я и new-I либо с помощью
git diff
, либо путем сравнения вывода git show I
и git show new-I
Особенно последним шагом будет тяжелая работа, если вы хотите сделать это полностью автоматически, по крайней мере, если были конфликты в фиксации