Ваш выбор вишни вызывает конфликт слияния, потому что одно и то же слияние вызовет конфликт слияния. Это потому, что выбор вишни является операцией слияния. Просто база слияния для вишневого выбора выбрана специально, а окончательный коммит — это обычный однородительский коммит, а не двухродительский коммит слияния.
Сам конфликт, я думаю, легче объяснить, когда он делается с git merge
. Предположим, у нас есть следующая серия коммитов:
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
Мы выбираем некоторую фиксацию, например фиксацию J
, в качестве текущей фиксации, выбирая одну имя ветки, например branch1
, в качестве текущей ветки. Git извлечет снимок в коммите J
в наше рабочее дерево:
git checkout branch1
Мне нравится обозначать эту ситуацию, добавляя специальное имя HEAD
к названию выбранной ветки, например:
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
Если мы сейчас запустим git merge branch2
, Git активирует механизм слияния, чтобы выполнить процесс слияния. Для этого механизма слияния требуется три коммита:
- Один из коммитов — наш текущий коммит
J
.
- Один из коммитов мы называем
git merge
: в данном случае branch2
выбирает коммит L
.
- Третий — или, в некотором смысле, первый, потому что Git должен использовать его первым — коммит — это тот, который находит процесс слияния. Команда
merge
находит наилучшую общую фиксацию: фиксацию, которая находится в обеих ветвях и в каком-то смысле ближе всего к двум фиксациям кончиков ветвей. Здесь этот коммит явно является коммитом H
.
Чтобы выполнить фактическую работу по слиянию, Git сейчас:
- сравнивает снимок в базе (
H
) со снимком в текущем коммите J
; а также
- сравнивает снимок в базе с другим коммитом
L
.
Это создает два различия: два рецепта изменения файлов, которые появляются в коммите базы слияния. В первом рецепте говорится, что если вы выполните различные действия, например, добавите и/или удалите определенные строки определенных файлов с файлами в H
, вы получите файлы в J
. Второй рецепт говорит, что если вы сделаете еще что-нибудь, то получите файлы в L
.
Механизм слияния теперь объединяет два различия. Такое объединение может привести к конфликтам слияния.
Вопрос о том, когда Git обнаруживает конфликт слияния, немного странный. Есть один случай, который достаточно ясен. Предположим, что базовая версия файла в коммите H
читает, например:
The quick brown fox
jumps over
the lazy dog.
Предположим, что версии J
и L
гласят:
The quick brown fox
sometimes jumps over
the lazy dog.
а также:
The quick brown fox
has often jumped over
the lazy dog.
Оба дифференциала изменили строку 2 несовместимым образом. Git не знает, какое изменение принять, поэтому он не принимает ни то, ни другое, в зависимости от того, как вы на это смотрите, а затем объявляет конфликт слияния и заставляет вас наводить порядок.
Ваши вишни выбирает
[Примечание: это было изменено, чтобы использовать график фиксации в обновленном вопросе. Теперь мы создаем репозиторий с:
mkdir tcherry && cd tcherry && git init
echo 1 > a.txt && git add a.txt && git commit -m 0x1
git checkout -b dev
echo 2 >> a.txt && git add a.txt && git commit -m 0x2
а затем либо вставьте 3
в начало a.txt
, либо добавьте его в конец a.txt
, чтобы сгенерировать настройку для двух сценариев:
printf "3\n1\n2\n" > a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev
or:
echo 3 >> a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev
Первый работает без конфликтов; второй дает конфликт.]
В вашем случае вы не используете git merge
, вы используете git cherry-pick
. Но вишневый выбор по-прежнему является слиянием. Вместо:
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
у вас есть:
B--C <-- dev
/
A <-- master (HEAD)
и вы запускаете git cherry-pick dev
. Обратите внимание, что dev
выбирает фиксацию C
, в то время как ваша текущая ветка — master
, а текущая фиксация — A
.
Команда cherry-pick вызывает механизм слияния Git, но на этот раз база слияния вынуждена быть родителем коммита, который вы выбираете. Таким образом, база слияния здесь — коммит B
. Теперь Git будет:
- diff
B
vs A
, чтобы получить наши изменения; а также
- diff
B
и C
, чтобы получить их изменения.
Разница между B
и A
заключается в том, что вы удалили строку, читающую 2
в конце a.txt
. Следующее (в котором используется хитрый тип git rev-parse
для поиска правильных коммитов; см. описание :/<text>
в документация gitrevisions) показывает это:
$ git diff :/0x2 :/0x1
diff --git a/a.txt b/a.txt
index 1191247..d00491f 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1 @@
1
-2
Разница между A
и C
зависит от того, какой из этих двух сценариев вы используете.
Вот разница для неконфликтного случая:
$ git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..0571a2e 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
+3
1
2
Это говорит о том, что нужно добавить 3
перед строкой, читающей 1
, которая была строкой 1 и становится строкой 2. Таким образом, Git должен удалить строку, читающую 2
, которая была строкой 2 в базовой версии слияния. Git может безопасно это сделать, потому что известно, где это сделать, и сама строка 1
не затрагивается: затрагиваются только строки со 2 до конца файла. Также предполагается, что Git вставляет строку, читающую 3
, перед строку, читающую 1
. Это действует от начала файла (строка 0) до строки 1. Затронутые диапазоны строк не перекрываются, а чтение строки 1
находится между ними, поэтому они также не соприкасаются друг с другом.
Теперь давайте рассмотрим случай, когда действительно возникает конфликт слияния:
$ git reset --hard HEAD^ # discard the cherry-pick
HEAD is now at 2b4180d 0x1
$ git checkout -q dev
$ git reset --hard HEAD^ # discard commit 0x3
HEAD is now at 1160130 0x2
$ echo 3 >> a.txt && git add a.txt && git commit -m 0x3
[dev c546f74] 0x3
1 file changed, 1 insertion(+)
$ cat a.txt
1
2
3
$ git checkout master
Switched to branch 'master'
Опять же, сейчас мы на коммите A
, и мы дадим Git указание выбрать вишневый коммит C
(тема 0x3
), чьим родителем является B
. Разница между B
и A
по-прежнему удаляет строку со значением 2 (которая находится в строке 2). Давайте посмотрим на разницу между B
и C
на этот раз:
git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..01e79c3 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
1
2
+3
Теперь предполагается, что Git объединит удаление строки с чтением 2
с добавлением строки с чтением 3
. Первый касается строки 2 (удалив ее полностью). Второй добавляет строку после строки 2.
Git, конечно, может объединить их, но алгоритму конфликта не нравится тот факт, что оба различия влияют на строку 2. В этих различиях нет строки между двумя изменениями, разделяющей изменения. друг от друга. Получаем конфликт:
$ git cherry-pick dev
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
error: could not apply c546f74... 0x3
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
Осмотрев оставленный a.txt
, мы видим:
$ cat a.txt
1
<<<<<<< HEAD
||||||| parent of c546f74... 0x3
2
=======
2
3
>>>>>>> c546f74... 0x3
Обратите внимание, что я установил merge.conflictStyle
на diff3
, поэтому я получаю строку ||||||| parent of ...
, показывающую, о чем конфликт. Сравните это со стилем по умолчанию:
$ git checkout -m --conflict merge a.txt
Recreated 1 merge conflict
$ cat a.txt
1
<<<<<<< ours
=======
2
3
>>>>>>> theirs
что лично я нахожу более загадочным. В случае выбора вишни, подобного этому, тот факт, что разница между B
и A
идет в обратном направлении, означает, что мы имеем удаление строки 2. Это формирует ours
часть конфликта. Это совсем не очевидно из конфликта стилей merge
; в стиле diff3
я могу посмотреть на это и увидеть, что в базовой версии слияния есть строка, читающая 2
, которую я удалил. Они сохранили строку 2, когда добавили строку 3
.
person
torek
schedule
22.01.2021