Есть ли разница между git rebase и git merge -ff-only

Из того, что я прочитал, они помогают нам получить линейную историю.

Из того, что я экспериментировал, rebase работает все время. Но merge -ff-only работает только в сценариях, где его можно быстро перенаправить.

Я также заметил, что git merge создает компиляцию слиянием, но если мы используем -ff-only, он дает линейную историю, которая по существу равна git rebasing. Так что -ff-only убивает цель git merge, правильно?

Так в чем же разница между ними?

Ответ 1

Обратите внимание, что git rebase имеет другое задание, чем git merge (с или без --ff-only). То, что rebase делает, - это взять существующие коммиты и скопировать их. Предположим, например, что вы находитесь на branch1 и сделали две коммиты A и B:

...-o--o--A--B   <-- HEAD=branch1
        \
         o--C    <-- branch2

и вы решите, что вы предпочтете, чтобы эти две фиксации были на branch2. Вы можете:

  • получите список изменений, внесенных вами в A (diff A против его родителя)
  • получите список изменений, внесенных вами в B (diff B против A)
  • перейти на branch2
  • внесите те же изменения, которые вы сделали в A, и скопируйте их, скопировав сообщение о фиксации с A; позвольте этому фиксатору A'
  • а затем внесите те же изменения, которые вы сделали в B, и скопируйте их, скопировав сообщение о фиксации с B; позвольте этому B'.

Здесь есть команда git, которая выполняет эту команду diff-and-then-copy-and-commit для вас: git cherry-pick. Итак:

git checkout branch2      # switch HEAD to branch2 (commit C)
git cherry-pick branch1^  # this copies A to A'
git cherry-pick branch1   # and this copies B

Теперь у вас есть это:

...-o--o--A--B         <-- branch1
        \
         o--C--A'-B'   <-- HEAD=branch2

Теперь вы можете вернуться к branch1 и удалить свои оригинальные A и B, используя git reset (я буду использовать --hard здесь, это более удобно, так как он очищает работу, дерево тоже):

git checkout branch1
git reset --hard HEAD~2

Это удаляет оригинальные A и B, 1 поэтому теперь у вас есть:

...-o--o               <-- HEAD=branch1
        \
         o--C--A'-B'   <-- branch2

Теперь вам просто нужно переустановить branch2, чтобы продолжить работу там.

Это то, что делает git rebase: он "движется" совершает (хотя и не перемещает их, потому что он не может: в git коммит никогда не может быть изменен, поэтому даже просто изменить родительский идентификатор требует скопировать его на новый и немного другой фиксатор).

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

Вышеописанное показывает перемещение коммитов из одной локальной ветки branch1 в другую локальную ветвь branch2, но git использует тот же самый процесс для перемещения, когда вы имеете ветвь удаленного отслеживания, которая приобретает некоторые новые коммиты, когда вы сделайте a git fetch (включая fetch, который является первым шагом git pull). Вы можете начать с работы с веткой feature, которая имеет вверх по течению от origin/feature и сделать пару собственных коммитов:

...-o        <-- origin/feature
     \
      A--B   <-- HEAD=feature

Но затем вы решаете, что вы должны увидеть, что произошло вверх по течению, поэтому вы запускаете git fetch, 2 и, aha, кто-то вверх по потоку написал commit C:

...-o--C     <-- origin/feature
     \
      A--B   <-- HEAD=feature

На этом этапе вы можете просто переустановить свои feature A и B на C, указав:

...-o--C     <-- origin/feature
        \
         A'-B'  <-- HEAD=feature

Это копии ваших оригинальных A и B, при этом оригиналы будут выброшены (но см. сноску 1) после завершения копирования.


Иногда нет ничего, что можно было бы переупаковать, т.е. никакой работы, которую вы сами делали. То есть график перед fetch выглядит так:

...-o      <-- origin/feature
           `-- HEAD=feature

Если вы введете git fetch и зафиксируете C, вы останетесь с веткой feature, указывающей на старый фиксатор, а origin/feature переместился вперед:

...-o--C   <-- origin/feature
     `---- <-- HEAD=feature

Здесь находится git merge --ff-only: если вы попросите объединить текущую ветвь feature с origin/feature, git видит, что можно просто сдвинуть стрелку вперед, так сказать, чтобы feature указывает непосредственно на commit C. Никакого фактического слияния не требуется.

Если у вас были свои собственные фиксации A и B, и вы попросили объединить их с C, git выполнит реальное слияние, сделав новое слияние commit M:

...-o--C        <-- origin/feature
     \   `-_
      A--B--M   <-- feature

Здесь --ff-only остановится и даст вам ошибку. С другой стороны, Rebase может скопировать A и B в A' и B', а затем скрыть исходные A и B.

Итак, короче (хорошо, слишком поздно:-)), они просто делают разные вещи. Иногда результат один и тот же, а иногда и нет. Если это ОК, чтобы скопировать A и B, вы можете использовать git rebase; но если есть какая-то веская причина не копировать их, вы можете использовать git merge, возможно, с --ff-only, чтобы слить-или-fail соответственно.


1 Git фактически сохраняет оригиналы в течение некоторого времени - обычно месяц в этом случае - но он скрывает их. Самый простой способ найти их - это git "reflogs", в котором хранится история того, где указана каждая ветвь, и где HEAD указывалось перед каждым изменением, которое обновляло ветку и/или HEAD.

В конце концов записи истории рефлогов истекают, и в этот момент эти коммиты становятся доступными для сборка мусора.

2Или, опять же, вы можете использовать git pull, что является удобством script, которое начинается с запуска git fetch. После того, как выборка выполнена, удобство script выполняется либо git merge, либо git rebase, в зависимости от того, как вы его конфигурируете и запускаете.

Ответ 2

Да, есть разница. git merge --ff-only прервется, если он не может выполнить ускоренную перемотку вперед, и берет фиксацию (обычно ветвь) для слияния. Он будет создавать коммит слияния, если он не может ускорить перемотку вперед (т.е. никогда не будет делать это с помощью --ff-only).

git rebase перезаписывает историю в текущей ветке или может использоваться для переустановки существующей ветки на существующую ветку. В этом случае он не будет создавать компиляцию слиянием, потому что он перезагружается, а не объединяется.

Ответ 3

Да, --ff-only всегда будет терпеть неудачу, если простой git merge завершится с ошибкой, и может завершиться неудачно, когда будет выполняться простой git merge. То, что точка - если вы пытаетесь сохранить линейную историю, и слияние не может быть выполнено таким образом, вы хотите, чтобы она потерпила неудачу.

Опция, которая добавляет случаи сбоя в команду, не бесполезна; это способ проверки предварительного условия, поэтому, если текущее состояние системы не соответствует ожидаемому, вы не делаете проблему хуже.