Почему Git знает, что может выбрать отменённый коммит?

В ветке, скажем, 3 коммита: A <- B <- C. Если я выберу B напрямую (Тест A), Git скажет:

The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:

    git commit --allow-empty

Я могу понять, что, поскольку B уже находится в этой ветке, выбрать ее снова невозможно.

Затем я вернул B и C в пакетную фиксацию:

git revert -n B^..C
git commit -a -m "xxx"

Это будет новая большая фиксация D, которая возвращает B и C, ветвь должна быть похожа на A <- B <- C <- D.

Затем мне нужно переделать B и C по какой-то причине. Я попытался:

git cherry-pick B^..C

Я вижу, что к ветке добавлены два новых коммита B' и C': A <- B <- C <- D <- B' <- C'.

Мой первый вопрос: как Git может разумно знать, что он должен создать B' и C'? Я думал, что Git обнаружит, что B и C уже находятся в истории веток, поэтому он может просто пропустить их, например, когда я выбираю 'B' непосредственно в Test A.

Затем, после этого, так как ветка уже A <- B <- C <- D <- B' <- C', я снова запускаю эту команду:

git cherry-pick B^..C

Я ожидал, что Git сможет распознать, что это неоперативная операция. Но на этот раз Git жалуется на конфликт. Мой второй вопрос: почему на этот раз Git не может распознать и пропустить эту операцию?


person xnervwang    schedule 24.01.2021    source источник


Ответы (3)


вишневый выбор - это слияние различий от родителя вашего вишневого выбора до вишневого выбора, с различиями от вашего родителя вишневого выбора до вашего проверенного наконечника. Вот и все. Git не должен знать больше. Ему все равно, где находятся какие-либо коммиты, ему нужно объединить эти два набора различий.

revert — это слияние различий от вашего возврата к его родителю с различиями от вашего возврата к проверенному совету. Вот и все. Git больше ничего знать не должен.

Вот: попробуйте это:

git init test; cd $_
printf %s\\n 1 2 3 4 5 >file; git add .; git commit -m1
sed -si 2s,$,x, file; git commit -am2
sed -si 4s,$,x, file; git commit -am3

Запустите git diff :/1 :/2 и git diff :/1 :/3. Это различия, которые запускает git, когда вы говорите git cherry-pick :/2 здесь. Первый diff изменяет строку 2, а второй коммит изменяет строки 2 и 4; изменение строки 4 не упирается ни в какие изменения в первом diff, а изменение строки 2 идентично в обоих случаях. Делать нечего, все изменения :/1-:/2 тоже в :/1-:/3.

Теперь, прежде чем вы начнете дальше, позвольте мне сказать следующее: это труднее объяснить прозой, чем просто увидеть. Выполните приведенную выше примерную последовательность и посмотрите на результат. намного, намного проще понять, что происходит, глядя на это, чем читая какое-либо описание. Каждый проходит через период, когда это слишком ново, и, возможно, небольшая ориентация поможет, и для этого и предназначены абзацы ниже, но опять же: проза сама по себе труднее понять, чем различия. Запустите diffs, попытайтесь понять, на что вы смотрите, если вам нужна небольшая помощь в том, что, как я обещаю, является очень небольшим горбом, следуйте тексту ниже. Когда он сфокусируется, посмотрите, не хлопнете ли вы хотя бы мысленно себя по лбу и не подумаете, почему это было так трудно увидеть?

Правила слияния Git довольно просты: идентичные изменения перекрывающихся или примыкающих строк принимаются как есть. Изменения строк без изменений в одном diff для измененных строк или строки, примыкающие к измененным строкам, в другом принимаются как есть. Разные изменения в любых перекрывающихся или примыкающих линиях, ну, есть очень много истории, на которую нужно смотреть, и никто никогда не находил правила, которое будет предсказывать, какие результаты должны быть каждый раз, поэтому git объявляет конфликт изменений, сбрасывает оба набора результатов в файл и позволяет вам решить, каким должен быть результат.

Итак, что произойдет, если вы теперь измените строку 3?

sed -si 3s,$,x, file; git commit -amx

запустите git diff :/1 :/2 и git diff :/1 :/x, и вы увидите, что там, где по отношению к родителю вишенки :/2 изменилась строка 2, а ваша подсказка изменила строки 2, 3 и 4. 2 и 3 соприкасаются, это исторически слишком близко для автоматизированных джинов, чтобы справиться правильно, так что да, вы можете это сделать: git cherry-pick :/2 теперь объявит конфликт, показав вам изменение строки 2 и две разные версии строк 3 и 4 (:/2 не изменил ни того, ни другого, ваш совет изменил оба, в контексте здесь ясно, что изменения в строках 3 и 4 хороши как есть, но опять же: никто так и не придумал автоматическое правило для надежной идентификации таких контекстов).

Вы можете прозвонить изменения в этой настройке, чтобы проверить, как работает возврат. Также всплывающие окна и слияния, а также git checkout -m, который запускает быстрое специальное слияние с вашим индексом.

Ваш git cherry-pick B^..C — это выборка из двух коммитов, B и C. Он делает их один за другим, точно так же, как описано выше. Поскольку вы вернули B и C, а затем снова выбрали их, это имеет тот же эффект, что и применение B и C, а затем выбор вишни B (с намерением затем выбрать вишни C). Я пришел к выводу, что B и C касаются перекрывающихся или примыкающих строк, поэтому git diff B^ B покажет изменения, которые перекрываются или примыкают к изменениям в git diff B^ C', и это то, что Git не собирается просто выбирать для вас, потому что все, что выглядит правильно здесь, в других обстоятельствах никто не может написать Правило идентификации, одинаково выглядящий выбор будет неправильным. Итак, git говорит, что два набора изменений конфликтуют, и вы должны разобраться.

person jthill    schedule 24.01.2021
comment
Разве вишневый выбор не является перебазированием? - person Mad Physicist; 24.01.2021
comment
@MadPhysicist Нет. Ребаза — это последовательность вишен. - person j6t; 24.01.2021
comment
@ j6t. Но это и не слияние, точно - person Mad Physicist; 24.01.2021
comment
@MadPhysicist Смотрите мой ответ. Коммит, полученный в результате выбора вишни, конечно, не является фиксацией слияния. Но способ, которым операция выбора вишни достигает своего результата, является операцией слияния, - person j6t; 24.01.2021
comment
Спасибо за подробное объяснение. Я думаю, что я неправильно понял в течение долгого времени. Я рассматривал git commit как файл diff, так как некоторое время использовал svn. Поэтому, на мой взгляд, фиксация не может быть воспроизведена дважды. Но поскольку на самом деле git commit использует моментальный снимок файла и алгоритм, такой как LCS, для сравнения моментальных снимков файлов, дублирование change можно игнорировать. Я говорю change, потому что git commit не имеет концепции изменения файла, а просто снимок файла, изменение файла рассчитывается в реальном времени, когда выполняются некоторые операции (например, слияние, выбор вишни и т. д.). Я прав? - person xnervwang; 24.01.2021
comment
У тебя вышло. Dag-of-snapshots с метками, все остальное просто смотрит на это, расширяет его, таскает его и перевешивает метки, все, что может быть полезно в вашей работе. - person jthill; 25.01.2021
comment
Эй, @jthill, мой bash вырвало на sed -si 2s,$,x, file, поэтому я не смог его попробовать. Я не sed человек, так что вы можете объяснить, что он должен делать? Спасибо. - person matt; 25.01.2021
comment
@matt добавьте x в конец строки 2. Какую версию bash вы используете для этого barfs? Попробуйте сначала заключить в одинарные кавычки бит 2…x,. редактировать: ... или это больше от каких-либо кадров в Apple, которые все еще пытаются произвести впечатление на своих второкурсников CS-класса? См. этот вопрос для некоторых из различия. - person jthill; 25.01.2021
comment
MacOS - вы даже не можете проверить версию sed. Eyeroll, см. stackoverflow.com/questions/37639496/ — думаю, я попрошу brew установить gsed для меня. - person matt; 25.01.2021
comment
Результат: да, детка. - person matt; 25.01.2021

Это расширяет ответ @jthill.

Рассмотрим обычное слияние в истории, подобное этому:

a--b--c--d--e--f--g--h
       \
        r--s--t

Git выполняет слияние, просматривая только содержимое этих коммитов:

c--h   <-- theirs
 \
  t    <-- ours
^
|
base

и ничего больше. Заметим, что на концептуальном уровне совершенно неважно, какая сторона обозначается нашей, а какая их; они полностью взаимозаменяемы. (Единственный раз, когда это имеет значение, это когда возникают конфликты, и Git должен решить, как он помечает стороны как их и наши для пользователя.) (Я буду опускать базу меток, их и нашу в следующих диаграммах.)

В вашей истории

A--B--C

операция слияния за первым git cherry-pick B рассматривала следующие коммиты:

A--B
 \
  C

Здесь выбран A, потому что он является родителем B, также известного как B^. Очевидно, что изменения с A на C также содержат изменения с A на B, и механизм слияния выдает результат слияния без изменений, а это создает сообщение cherry-pick is now empty.

Затем вы сделали эту историю, изменив B и C:

A--B--C--R

Затем следующий git cherry-pick B просмотрел эти коммиты:

A--B
 \
  R

На этот раз изменения с A на R больше не содержат изменений с A на B, поскольку они были отменены. Таким образом, слияние больше не приводит к пустому результату.

Небольшое отступление: когда вы делаете git revert B в своей истории, механизм слияния просматривает следующие коммиты:

B--A
 \
  C

Обратите внимание, что только B и родитель B, также известный как A, меняются местами по сравнению с git cherry-pick B.

(Я описывал отмену с одной фиксацией, поскольку не уверен, как работает отмена с несколькими фиксациями.)

person j6t    schedule 24.01.2021
comment
Отмена множественной фиксации, с git revert -n, просто повторяет каждую обратную выборку вишни без фиксации. Git обновляет и индекс, и рабочее дерево, чтобы они были синхронизированы для следующего шага после каждого шага. (Обратите внимание, что наша фиксация, используемая для слияния, — это то, что находится в индексе: вы можете создать беспорядок, если у вас не синхронизированы индекс и рабочее дерево.) - person torek; 24.01.2021
comment
Спасибо, @torek, за разъяснения. Я не буду писать это в ответе, так как это всего лишь обходной путь. - person j6t; 24.01.2021

Давайте отступим здесь примерно на десять футов и получим более широкую ментальную картину того, что такое Git.

Коммит Git — это моментальный снимок всех файлов. По сути, он представляет весь ваш проект. Дело не в дифах. Это блестящая архитектура, потому что она чрезвычайно быстра и практически безошибочна. Любая фиксация может полностью восстановить это состояние вашего проекта, просто проверив его; нет нужды думать.

Однако Git может создавать различия между двумя коммитами, и именно так он реализует то, что мы можем назвать логикой слияния. Каждое слияние состоит из одновременного применения двух различий. [Ну, их может быть больше двух, но притворитесь, что это не так.] Слияние, выбор вишни, перебазирование, возврат — все это слияния в этом смысле — все они используют логику слияния для формирования коммита, выражающего результат применение двух дифф. Хитрость заключается в том, чтобы знать, кто сравнивается при построении двух различий.

  • Когда вы запрашиваете истинное git merge, скажем, двух ветвей, Git выясняет, где эти ветки расходились в последний раз. Это называется база слияния. Сравнимы: основание слияния и конец ветви 1, а также основание слияния и вершина ветви 2. Оба этих двух различия применяются к базе слияния, и результат используется для формирования коммита с двумя родителями (кончики ветвей). Затем имя первой ветки сдвигается на единицу вверх, указывая на этот новый коммит.

  • Когда вы запрашиваете cherry-pick, база слияния является родителем выбираемой фиксации. Сравнимы: база слияния и голова, а также база слияния и выбранная фиксация. Оба этих двух различия применяются к базе слияния, и результат используется для формирования фиксации с одним родителем (головой). Затем имя головной ветки сдвигается вверх на единицу, указывая на этот новый коммит. [А перебазирование — это просто набор вишен!]

  • revert также использует логику слияния. Как объяснил jthill, достаточно сформировать один из diff назад. База слияния — это фиксация, которую вы пытаетесь отменить. Сравнимы: база слияния и ее родитель (в этом направлении), а также база слияния и начало. Эти различия применяются к базе слияния и используются для формирования коммита, родителем которого является голова. Затем имя головной ветки сдвигается вверх на единицу, указывая на этот новый коммит. Если это наводит вас на мысль, что возврат — это, по сути, обратная выборка вишни, вы абсолютно правы.


Круто то, что, зная это, вы можете предсказать, что произойдет, когда вы дадите одну из этих команд, потому что вы можете извлечь те же самые различия самостоятельно, произнеся git diff. Логика слияния Git, по сути, открыта для вашего взгляда. Тогда остается только понять обстоятельства, при которых Git останавливается посреди операции, потому что он не может продолжить работу без дальнейших явных инструкций. Это называется (к сожалению) конфликтом, и он может возникнуть двумя основными способами:

  • Одна и та же строка в одном и том же файле была изменена двумя разными способами в двух различиях. Представление Git о том, что представляет собой одна и та же строка, гораздо шире, чем вы могли бы ожидать; это удивляет новичков.

  • Один и тот же файл, qua file, обрабатывался двумя несовместимыми способами: например, один diff его удаляет, а другой diff сохраняет и редактирует.


Я должен добавить еще один факт, который объясняет многое в поведении, в том числе часть того, о чем вы спрашиваете. Это может показаться очевидным, но стоит четко заявить: в diff ничего не является вещью. Я имею в виду вот что. Предположим, что один diff изменяет строку, а другой diff ничего не делает с этой строкой. Тогда способ активировать оба различия: изменить строку. Ничегонеделание — это не вещь: оно не борется с изменением.

Это стоит упомянуть, особенно потому, что новички часто не понимают этого. На днях был вопрос, когда пользователь жаловался, что при слиянии, когда вторая ветвь удалила файл, файл действительно был удален, хотя первая ветвь сохранила его. Этот пользователь думал о том, чтобы не удалять файл как вещь, и даже как первичную вещь. Но это не так. Два различия по умолчанию имеют одинаковый вес, поэтому одна ветвь ничего не сделала, а другая ветвь удалила файл, и ничего не делать - это не вещь, поэтому результатом является удаление файла.

person matt    schedule 24.01.2021