В чем разница между git cherry-pick и git format-patch | git am?

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

git cherry-pick tags/myfix

Это работает, но выбор вишни занимает все больше времени при «неточном обнаружении переименования».

Я подозревал, что это может быть быстрее с

git format-patch -k -1 --stdout tags/myfix | git am -3 -k

На самом деле, это помогло применить исправление мгновенно, оставив мою ветку точно в том же состоянии, что и выбор вишни.

Теперь мой вопрос: что именно делает выбор вишни по-другому? Я думал, что выбор вишни в основном реализован именно так, но я, должно быть, ошибался.


person Felix Dombek    schedule 31.08.2018    source источник
comment
Разве -X no-renames не отключает это?   -  person o11c    schedule 31.08.2018
comment
@ o11c - Да, должен. У меня нет привычки использовать это, так как это может незаметно испортить результат слияния, но в тех случаях, когда вы знаете, что это безопасно, это позволит избежать потери времени.   -  person Mark Adelsberger    schedule 31.08.2018


Ответы (2)


cherry-pick реализован как слияние, при этом база слияния является родителем cmomit, который вы вносите. В случаях, когда нет конфликтов слияния, это должно иметь точно такой же эффект, как создание и применение исправления, как у вас (но см. ответ Торека для небольшого предостережения, где am теоретически может сделать что-то не так).

Но, выполняя слияние, cherry-pick может попытаться более изящно обрабатывать случаи, когда изменения могут конфликтовать. (На самом деле опция -3, которую вы дали am, говорит ему, что в случае необходимости он должен делать то же самое, если у него достаточно контекста в патче, чтобы сделать это. Я вернусь к этому моменту в конец...)

Когда вы применяете патч, по умолчанию, если он изменяет фрагмент кода, который не совпадает с тем коммитом, в котором вы его применяете, как и в родительском коммите, из которого он был сгенерирован, то применение завершится ошибкой. Но подход cherry-pick/merge рассмотрит эти различия и создаст из них конфликт слияния, так что у вас есть шанс разрешить конфликт и продолжить работу.

Как часть обнаружения конфликтов, cherry-pick выполняет обнаружение переименования. Так, например, скажем, у вас есть

o -- x -- x -- A <--(master)
      \
       B -- C -- D <--(feature)

и вы cherry-pick передаете C на master. Предположим, в o вы создали file.txt, а в A у вас есть модификации file.txt. Но фиксация B перемещает file.txt в my-old-file.txt, а фиксация C изменяет my-old-file.txt.

Изменение на my-old-file.txt в C может конфликтовать с изменением на file.txt в A; но чтобы увидеть эту возможность, git должен выполнить обнаружение переименования, чтобы он мог понять, что file.txt и my-old-file.txt - это «одно и то же».

Вы можете знать, что у вас нет такой ситуации, но git не знает, пока не попытается обнаружить переименования. Я не уверен, почему в данном случае это отнимает много времени; по моему опыту, обычно это не так, но в репозитории с большим количеством добавленных и удаленных путей (между B и либо C, либо A в нашем примере) это может быть.

Когда вместо этого вы создаете и применяете исправление, оно пытается применить исправление, полагая, что конфликта нет. Только если это столкнется с проблемой (и только потому, что вы указали параметр -3), он вернется к выполнению слияния с обнаружением конфликтов. Он может пропустить все это - и любое потенциальное обнаружение переименования - до тех пор, пока его первая попытка применяется чисто.


Обновление. Как отмечено в комментариях к вопросу, вы также можете отключить обнаружение переименования, если оно не помогает и работает медленно. Если вы используете это, когда на самом деле есть переименования, которые «имеют значение» для слияния, это может вызвать конфликты, в которых обнаружение переименования разрешит их. Хотя я не думаю, что это необходимо, я не могу исключить, что он также может просто вычислить неверный результат слияния и незаметно применить его — поэтому я редко использую эту опцию.

Для стратегии слияния по умолчанию параметр -X no-renames отключает обнаружение переименования. Вы можете передать эту опцию cherry-pick.

Согласно комментарию Торека, кажется, что обнаружение переименования не должно быть проблемой с am. Тем не менее, я могу подтвердить, что он способен правильно обрабатывать случай, когда слияние работает только с обнаружением переименования. Я собираюсь вернуться к попыткам понять все тонкости этого как-нибудь, когда не будет вечер пятницы.

person Mark Adelsberger    schedule 31.08.2018
comment
Когда git am возвращается к трехстороннему слиянию, уже слишком поздно: нет возможности обнаружить переименование. Git уже выполнил идентификацию файлов! (Код am использует хэши больших двоичных объектов в строках index, как показано в выводе git diff. На данный момент не осталось имен файлов.) - person torek; 31.08.2018
comment
@torek Хм .... В пятницу мне уже слишком поздно думать об этом, но я поверю вам на слово и обновлю соответственно. Позже постараюсь полностью разобраться :) - person Mark Adelsberger; 31.08.2018

Ответ Марка Адельсбергера правильный (за него проголосовали, и, вероятно, вы должны его принять). Но здесь следует отметить историческую странность.

На самом деле, когда-то Cherry-pick был реализован как git format-patch | git am -3, а git rebase до сих пор использует этот конкретный метод копирования коммитов для некоторых видов перебазирования.1 Проблема здесь в том, что это не работает. для обнаружения переименованных файлов, а иногда — в (редких) условиях, которые трудно описать, но я попытаюсь — неправильно применить изменения там, где правильное трехстороннее слияние применит их правильно. Рассмотрим, например, этот случай:

@@ -123,5 ... @@
     }
   }
-  thing();
   {
     {

где окружающий контекст вокруг удаленной строки — это просто фигурные скобки (или, что еще хуже, пробелы) — другими словами, что-то бесполезно совпадающее. В вашей версии этого же файла из-за какого-то другого события были строки с 123 по 127, теперь они находятся раньше или позже в том же файле. Допустим, например, что теперь это строки 153-158. Тем временем строки 123-127 в вашем файле гласят:

    }
  }
  thing();
  {
    {

но эти строки верны: вызов thing(), который следует удалить (потому что он неправильный), переместился вниз, но есть вызов thing(), который не следует удалять, в то самое место.

Трехстороннее слияние сравнит базу слияния с вашей версией и, возможно — возможно, в зависимости от удачи и контекста, допускающего сравнение — обнаружит, что вы вставили разные строки, чтобы ошибочный вызов, который следует удалить, теперь находится в строке 155, а не в строке 125. Затем он выполнит правильное удаление, поскольку знает, что то, что было базовой строкой 125, является вашей строкой 155.

Обнаружение переименования является более важным из этих двух различий между форматированием-исправлением-затем-применением и истинным трехсторонним слиянием, но в некоторых случаях оба имеют значение. Запуск git cherry-pick делает более тщательную, медленную и чаще правильную вещь.


1В частности, только неинтерактивный git rebase когда-либо использует format-patch, и даже в этом случае, только если вы не используете параметр -m, укажите стратегию слияния с -s или укажите расширенный параметр с помощью -X. Любой из этих трех способов заставляет неинтерактивную перебазировку использовать метод выбора вишни.

Обратите внимание, что документация Git называет аргументы -X «параметрами стратегии» или «аргументами варианта стратегии», что в любом случае является очень неуклюжей фразой. Мне нравится здесь слово «расширенный», так как оно объясняет, почему это -X, т. е. расширенный. Расширенная опция — это просто опция, переданная в стратегии, которую вы выбираете с помощью -s: Git не знает, какие дополнительные опции понимает каждая -s, поэтому все, что вы даете -X, Git передает выбранной стратегии, а тогда сама стратегия либо принимает опцию -X и что-то делает, либо жалуется на нее как на неизвестную.

person torek    schedule 31.08.2018