Коммиты Git дублируются в той же ветке после выполнения перебазирования

131

Я понимаю сценарий, представленный в Pro Git об опасностях ребазинга . Автор в основном объясняет, как избежать дублирования коммитов:

Не перемещайте коммиты, которые вы отправили в публичный репозиторий.

Я собираюсь рассказать вам свою конкретную ситуацию, потому что я думаю, что она не совсем соответствует сценарию Pro Git, и у меня все еще остаются дублированные коммиты.

Допустим, у меня есть две удаленные ветки с их локальными аналогами:

origin/master    origin/dev
|                |
master           dev

Все четыре ветки содержат одинаковые коммиты, и я собираюсь начать разработку в dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

После пары коммитов я нажимаю изменения на origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Я должен вернуться к, masterчтобы быстро исправить:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

И вернемся к тому, что devя переустановил изменения, чтобы включить быстрое исправление в мою фактическую разработку:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Если я покажу историю коммитов с помощью GitX / gitk, я замечаю, что origin/devтеперь содержит два идентичных коммита C5'иC6' которые отличаются от Git. Теперь, если я внесу изменения, origin/devэто будет результат:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Возможно, я не совсем понимаю объяснение в Pro Git, поэтому я хотел бы знать две вещи:

  1. Почему Git дублирует эти коммиты при перемещении? Есть ли особая причина делать это вместо того, чтобы просто применять C5иC6 после C7?
  2. Как мне этого избежать? Было бы разумно это сделать?
elitalon
источник

Ответы:

87

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

  • C5и C6временно выведены изdev
  • C7 применяется к dev
  • C5и C6воспроизводятся поверх C7, создавая новые различия и, следовательно, новые коммиты

Итак, в вашей devветке C5и C6фактически больше нет: их сейчас C5'и C6'. Когда вы нажимаете на origin/dev, git видит C5'и C6'как новые коммиты и прикрепляет их к концу истории. Действительно, если вы посмотрите на различия между C5и C5'вorigin/dev , вы заметите, что, хотя содержимое одинаково, номера строк, вероятно, разные, что отличает хеш фиксации.

Я повторю правило Pro Git: никогда не перемещайте коммиты, которые когда-либо существовали где-либо, кроме вашего локального репозитория . Вместо этого используйте слияние.

Джастин ᚅᚔᚈᚄᚒᚔ
источник
У меня такая же проблема, как я могу исправить историю удаленных веток сейчас, есть ли другой вариант, кроме удаления ветки и воссоздания ее с помощью выбора вишни?
Wazery
1
@xdsy: Посмотри на это и на это .
Джастин ᚅᚔᚈᚄᚒᚔ
2
Вы говорите: «C5 и C6 временно отключены от dev ... C7 применяется к dev». Если это так, то почему C5 и C6 появляются перед C7 в упорядочении коммитов в origin / dev?
KJ50 05
@ KJ50: Потому что C5 и C6 уже были переведены origin/dev. При devперебазировании его история изменяется (C5 / C6 временно удаляется и повторно применяется после C7). Изменение истории отправленных репозиториев обычно является действительно плохой идеей ™, если вы не знаете, что делаете. В этом простом случае проблему можно решить, выполнив принудительное нажатие с devна origin/devпосле перебазирования и уведомив всех, кто работает над origin/devэтим, о том, что у них, вероятно, будет плохой день. И снова лучший ответ - «не делайте этого ... используйте вместо этого слияние»
Джастин ᚅᚔᚈᚄᚒᚔ
3
Одно замечание: хеш-коды C5 и C5 ', безусловно, разные, но не из-за разных номеров строк, а из-за следующих двух фактов, из которых любого достаточно для разницы: 1) хеш, о котором мы говорим - это хэш всего исходного дерева после фиксации, а не хэш разницы дельты, и поэтому C5 'содержит все, что исходит от C7, а C5 - нет, и 2) Родитель C5' отличается от C5, и эта информация также включен в корневой узел дерева фиксации, влияющий на результат хеширования.
Озгур Мурат
113

Короткий ответ

Вы пропустили тот факт, что вы запустили git push, получили следующую ошибку и затем продолжили работу git pull:

To git@bitbucket.org:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Несмотря на то, что Git пытается быть полезным, его совет «git pull», скорее всего, не то, что вы хотите делать .

Если ты:

  • Работая над «ответвлением новой функции» или «разработчик филиалом» только , то вы можете запустить , git push --forceчтобы обновить пульт с вашими после перебазирования фиксаций ( в соответствии с ответом user4405677 игровых ).
  • Если вы работаете в ветке с несколькими разработчиками одновременно, вам, вероятно, не следует использовать егоgit rebase в первую очередь. Чтобы обновить devс изменениями из master, вы должны вместо запуска git rebase master devзапускать git merge masterво время работы dev( согласно ответу Джастина ).

Немного более длинное объяснение

Каждый хэш фиксации в Git основан на нескольких факторах, один из которых - хэш предшествующей фиксации.

Если вы измените порядок коммитов, вы измените хеши коммитов; rebase (когда он что-то делает) изменит хеши коммитов. При этом результат выполнения git rebase master dev, с которым devне синхронизирован master, создаст новые коммиты (и, следовательно, хэши) с тем же содержанием, что и те, которые включены, devно с коммитами, masterвставленными перед ними.

Вы можете попасть в такую ​​ситуацию разными способами. Я могу думать о двух способах:

  • У вас могут быть коммиты, на masterкоторых вы хотите основывать своиdev работу
  • У вас могли быть коммиты dev, которые уже были отправлены на удаленный компьютер, которые вы затем переходите к изменению (перефразируйте сообщения коммитов, переупорядочивайте коммиты, сквош-коммиты и т.

Давайте лучше разберемся, что произошло - вот пример:

У вас есть репозиторий:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Начальный набор линейных коммитов в репозитории

Затем вы переходите к изменению коммитов.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Здесь вам придется поверить мне на слово: есть несколько способов изменить коммиты в Git. В этом примере я изменил время C3, но вы будете вставлять новые коммиты, изменять сообщения коммитов, переупорядочивать коммиты, объединение коммитов и т. д.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

То же самое с новыми хешами

Здесь важно заметить, что хеши коммитов разные. Это ожидаемое поведение, поскольку вы что-то (что-то) в них изменили. Это нормально, НО:

Графический журнал, показывающий, что главный компьютер не синхронизирован с удаленным

Попытка нажать покажет вам ошибку (и намекнет, что вам следует запустить git pull).

$ git push origin master
To git@bitbucket.org:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Если мы запустим git pull, то увидим такой лог:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Или, как показано по-другому:

Журнал графика, показывающий фиксацию слияния

И теперь у нас есть дублирующиеся коммиты локально. Если бы мы запустили, git pushмы бы отправили их на сервер.

Чтобы не попасть на этот этап, мы могли бежать git push --force(там, где мы бежали git pull). Это без проблем отправило бы наши коммиты с новыми хешами на сервер. Чтобы решить проблему на этом этапе, мы можем вернуться к предыдущему состоянию git pull:

Посмотрите на reflog ( git reflog) , чтобы увидеть , что совершить хэш был до того мы побежали git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Выше мы видим, что это ba7688aбыла фиксация, в которой мы были до запуска git pull. Имея в руках этот хеш фиксации, мы можем вернуться к этому ( git reset --hard ba7688a) и затем запустить git push --force.

И мы закончили.

Но подождите, я продолжал основывать работу на дублированных коммитах.

Если вы каким-то образом не заметили, что коммиты были дублированы, и продолжили работать поверх повторяющихся коммитов, вы действительно навредили себе. Размер беспорядка пропорционален количеству коммитов, которые у вас есть поверх дубликатов.

Как это выглядит:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Журнал Git показывает линейные коммиты поверх дублированных коммитов

Или, как показано по-другому:

Журнальный график, показывающий линейные коммиты поверх дублированных коммитов

В этом сценарии мы хотим удалить повторяющиеся коммиты, но сохранить основанные на них коммиты - мы хотим сохранить от C6 до C10. Как и в большинстве случаев, есть несколько способов сделать это:

Либо:

  • Создайте новую ветку при последней дублированной фиксации 1 ,cherry-pick каждую фиксацию (с C6 по C10 включительно) в эту новую ветку и рассматривайте эту новую ветвь как каноническую.
  • Выполнить git rebase --interactive $commit, где $commitнаходится фиксация перед обеими дублированными коммитами 2 . Здесь мы можем сразу удалить строки для дубликатов.

1 Не имеет значения , какой из двух вы выбираете, либо ba7688aили 2a2e220нормально работать.

2 В примере это было бы 85f59ab.

TL; DR

Набор advice.pushNonFastForwardдля false:

git config --global advice.pushNonFastForward false
Whymarrh
источник
1
Можно следовать совету «git pull ...», если вы понимаете, что многоточие скрывает параметр «--rebase» (он же «-r»). ;-)
G. Sylvie Davies
4
Я бы рекомендовал использовать git push«S в --force-with-leaseнастоящее время , как это лучше по умолчанию
Whymarrh
4
Либо этот ответ, либо машина времени. Спасибо!
ZeMoon
Очень аккуратное объяснение ... Я наткнулся на аналогичную проблему, которая дублировала мой код 5-6 раз после того, как я неоднократно пытался перебазировать ... просто чтобы убедиться, что код обновлен с помощью мастера ... но каждый раз, когда он нажимал new фиксируется в моей ветке, дублируя мой код. Не могли бы вы сказать мне, безопасно ли здесь принудительное нажатие (с возможностью аренды), если я единственный разработчик, работающий над моей веткой? Или лучше слить мастер с моим вместо перебазирования?
Дхрув
12

Я думаю, вы упустили важную деталь при описании своих шагов. Точнее, ваш последний шаг git pushв dev фактически дал бы вам ошибку, поскольку вы обычно не можете продвигать изменения, отличные от fastforward.

Так вы сделали git pullперед последним нажатием, что привело к слиянию с C6 и C6 'в качестве родителей, поэтому оба останутся в журнале. Более красивый формат журнала мог бы сделать более очевидным, что они являются объединенными ветвями дублированных коммитов.

Или вы вместо этого сделали git pull --rebase(или без явного, --rebaseесли это подразумевается вашей конфигурацией), который вернул исходные C5 и C6 в ваш локальный разработчик (и затем повторно перебазировал следующие на новые хеши, C7 'C5' 'C6' «).

Одним из выходов из этого могло быть git push -fпринудительное нажатие, когда оно выдавало ошибку, и стирание C5 C6 из источника, но если бы кто-то еще вытащил их, прежде чем вы стерли их, у вас было бы намного больше проблем .. .. По сути, каждому, у кого есть C5 C6, нужно будет предпринять специальные шаги, чтобы избавиться от них. Именно поэтому они говорят, что никогда не следует перебазировать уже опубликованное. Тем не менее, это все еще выполнимо, если упомянуть, что «публикация» осуществляется небольшой командой.

user4405677
источник
1
Пропуск git pullимеет решающее значение. Ваша рекомендация git push -f, хотя и опасна, вероятно, именно то, что ищут читатели.
Whymarrh
На самом деле. Когда я писал вопрос, я действительно сделал это git push --force, просто чтобы посмотреть, что Git собирается делать. С тех пор я много узнал о Git, и сейчас rebaseэто часть моего обычного рабочего процесса. Однако я делаю это, git push --force-with-leaseчтобы не перезаписывать чужую работу.
elitalon 07
Использование --force-with-leaseпо умолчанию - хороший вариант, я также оставлю комментарий под своим ответом
Whymarrh
2

Я обнаружил, что в моем случае эта проблема является следствием проблемы конфигурации Git. (С привлечением вытягивания и слияния)

Описание проблемы:

Симптомы: коммиты дублируются в дочерней ветке после перебазирования, что подразумевает многочисленные слияния во время и после перебазирования.

Рабочий процесс: вот шаги рабочего процесса, который я выполнял:

  • Работа над "Возможностями-веткой" (дочерняя ветка "Разработка-ветка")
  • Зафиксируйте и отправьте изменения в "Features-branch"
  • Оформить заказ "Develop-branch" (материнская ветка Features) и работать с ней.
  • Зафиксируйте и отправьте изменения в "Develop-branch"
  • Оформить заказ "Features-branch" и получить изменения из репозитория (на случай, если кто-то другой совершил работу)
  • Перебазируйте "Features-branch" на "Develop-branch"
  • Нажать силу изменений на «Feature-ветку»

Как следствие этого рабочего процесса, дублирование всех коммитов «Feature-branch» с момента предыдущего перебазирования ... :-(

Проблема возникла из-за того, что дочерняя ветка внесла изменения перед перебазированием. Конфигурация вытягивания Git по умолчанию - «слияние». Это меняет индексы коммитов, выполненных в дочерней ветке.

Решение: в файле конфигурации Git настройте pull для работы в режиме перебазирования:

...
[pull]
    rebase = preserve
...

Надеюсь, это поможет JN Grx

JN Gerbaux
источник
1

Возможно, вы использовали удаленную ветку, отличную от текущей. Например, вы могли использовать Master, когда ваша ветка разрабатывает отслеживание разработки. Git послушно выполнит дублирующиеся коммиты, если они будут извлечены из не отслеживаемой ветки.

В этом случае вы можете сделать следующее:

git reset --hard HEAD~n

где n == <number of duplicate commits that shouldn't be there.>

Затем убедитесь, что вы используете правильную ветку, а затем запустите:

git pull upstream <correct remote branch> --rebase

Использование with --rebaseгарантирует, что вы не добавите посторонние коммиты, которые могут запутать историю коммитов.

Вот небольшой пример git rebase.

ScottyBlades
источник