Git рабочий процесс и ребаз против вопросов слияния - PullRequest
928 голосов
/ 19 января 2009

Я использую Git уже пару месяцев в проекте с одним другим разработчиком. У меня есть несколько лет опыта работы с SVN , поэтому я думаю, что привнесу много багажа в отношения.

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

Мой партнер говорит мне, что мои проблемы проистекают из моего желания слиться волей-неволей, и что во многих ситуациях мне следует использовать rebase вместо слияния. Например, вот рабочий процесс, который он заложил:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

По сути, создайте ветвь объекта, ВСЕГДА перебирайте с мастера на ветку и объединяйте ветку обратно с мастером. Важно отметить, что филиал всегда остается локальным.

Вот рабочий процесс, с которого я начал

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

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

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

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

Во всяком случае, я следил за своим рабочим процессом в недавней ветке, и когда я попытался объединить его с мастером, все пошло к черту. Было множество конфликтов с вещами, которые не должны были иметь значения. Конфликты просто не имели смысла для меня. Мне потребовался день, чтобы разобраться во всем, и в итоге я вынужден был принудительно передать удаленному мастеру, поскольку мой локальный мастер разрешил все конфликты, но удаленный все еще не был доволен.

Каков «правильный» рабочий процесс для чего-то подобного? Предполагается, что Git сделает ветвление и слияние очень простым, и я просто не вижу этого.

Обновление 2011-04-15

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

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

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

На самом деле наш рабочий процесс немного отличается, так как мы склонны делать слияния сквоша вместо необработанных слияний. ( Примечание: это противоречиво, см. Ниже. ) Это позволяет нам превратить всю нашу ветку объектов в один коммит на master. Затем мы удаляем нашу ветку функций. Это позволяет нам логически структурировать наши коммиты на master, даже если они немного запутаны в наших ветках. Итак, вот что мы делаем:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

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

Github и Bitbucket (другие?) Pull Requests - Если вам интересно, как слияние / ребазирование связано с Pull Requests, я рекомендую выполнять все вышеописанные шаги, пока вы не будете готовы к слиянию освоить. Вместо ручного слияния с git, вы просто принимаете пиар. Обратите внимание, что это не будет выполнять слияние с помощью сквоша (по крайней мере, по умолчанию), но без сквоша, без ускоренной перемотки является принятым соглашением о слиянии в сообществе запросов на извлечение (насколько я знаю). В частности, это работает так:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

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

Ответы [ 10 ]

372 голосов
/ 27 июня 2012

TL; DR

Рабочий процесс git rebase не защищает вас от людей, плохо разбирающихся в разрешении конфликтов, или от людей, привыкших к рабочему процессу SVN, как это предложено в Предотвращение катастроф Git: история Gory . Это только делает разрешение конфликтов более утомительным для них и затрудняет восстановление после плохого разрешения конфликтов. Вместо этого используйте diff3, чтобы не было так сложно в первую очередь.


Рабочий процесс переназначения не лучше для разрешения конфликтов!

Я очень сторонник перебазирования за очистку истории. Однако, если я когда-либо столкнулся с конфликтом, я немедленно прекращаю ребазирование и вместо этого выполняю слияние! Меня действительно убивает, что люди рекомендуют рабочий процесс ребазирования в качестве лучшей альтернативы рабочему процессу слияния для разрешения конфликтов (который является именно о чём был этот вопрос).

Если во время слияния он пойдет "все в ад", то при перебазировании он пойдет "все в ад", и, возможно, намного больше в ад! И вот почему:

Причина № 1: Разрешать конфликты один раз, а не один раз для каждого коммита

Когда вы перебазируете вместо слияния, вам придется выполнять разрешение конфликтов столько раз, сколько вы совершаете перебазирование, для одного и того же конфликта!

Реальный сценарий

Я разветвляюсь от мастера, чтобы провести рефакторинг сложного метода в ветке. Моя работа по рефакторингу состоит из 15 коммитов, так как я работаю над ее рефакторингом и получаю обзоры кода. Часть моего рефакторинга включает исправление смешанных вкладок и пробелов, которые присутствовали в master раньше. Это необходимо, но, к сожалению, оно будет конфликтовать с любыми изменениями, внесенными впоследствии в этот метод в master. Конечно же, пока я работаю над этим методом, кто-то вносит простое, законное изменение в тот же метод в основной ветке, который должен быть объединен с моими изменениями.

Когда пришло время объединить мою ветку с master, у меня есть два варианта:

git merge: Я получаю конфликт. Я вижу изменения, которые они сделали, чтобы освоить и объединить их с (конечным продуктом) моей ветки. Готово.

git rebase: Я получаю конфликт с моим первым коммитом. Я разрешаю конфликт и продолжаю перебазирование. Я получаю конфликт с моим вторым коммитом. Я разрешаю конфликт и продолжаю перебазирование. Я получил конфликт с моим третьим коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим четвертым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим пятым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим шестым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим седьмым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим восьмым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим девятым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим десятым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим одиннадцатым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим двенадцатым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим тринадцатым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим четырнадцатым коммитом. Я разрешаю конфликт и продолжаю перебазирование. У меня конфликт с моим пятнадцатым коммитом. Я разрешаю конфликт и продолжаю перебазирование.

Вы, должно быть, шутите, если это ваш предпочитаемый рабочий процесс. Все, что требуется, - это исправление пробелов, которое конфликтует с одним изменением, внесенным в master, и каждый коммит будет конфликтовать и должен быть разрешен. И это простой сценарий с конфликтом только пробелов. Не дай Бог, у вас есть реальный конфликт, связанный с серьезными изменениями кода между файлами, и вам нужно разрешить , что несколько раз.

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

Причина № 2: Отмена не отменяется!

Я думаю, что мы все можем согласиться с тем, что урегулирование конфликта может быть трудным, а также что некоторые люди очень плохи в этом. Он может быть очень подвержен ошибкам, поэтому он так хорош, что git позволяет легко его отменять!

Когда вы объединяете ветку, git создает коммит слияния, который можно отменить или изменить, если разрешение конфликта идет плохо. Даже если вы уже отправили неудачный коммит слияния в публичное / авторитетное репо, вы можете использовать git revert, чтобы отменить изменения, внесенные слиянием, и правильно повторить слияние в новом коммите слияния.

Когда вы перебазируете ветку, в вероятном случае, если разрешение конфликта сделано неправильно, вы облажались. Каждый коммит теперь содержит неверное слияние, и вы не можете просто повторить ребаз *. В лучшем случае вы должны вернуться и внести поправки в каждый из затронутых коммитов. Не смешно.

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

* Может быть возможно отменить ребазинг, если вы можете выкопать старые ссылки из внутренних журналов git или если вы создадите третью ветку, которая указывает на последний коммит перед ребазингом.

Уберись от разрешения конфликта: используй diff3

Возьмем этот конфликт, например:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Глядя на конфликт, невозможно сказать, что изменилось в каждой ветви или каково ее намерение. По моему мнению, это самая главная причина, по которой разрешение конфликтов является запутанным и трудным.

diff3 на помощь!

git config --global merge.conflictstyle diff3

Когда вы используете diff3, каждый новый конфликт будет иметь 3-й раздел, объединенный общий предок.

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Сначала изучите объединенного общего предка. Затем сравните каждую сторону, чтобы определить намерение каждой ветви. Вы можете видеть, что HEAD изменил EmailMessage на TextMessage. Его цель - изменить используемый класс на TextMessage, передавая те же параметры. Вы также можете видеть, что намерение ветви функции - передать false вместо true для опции: include_timestamp. Чтобы объединить эти изменения, объедините намерение обоих:

TextMessage.send(:include_timestamp => false)

В целом:

  1. Сравните общего предка с каждой ветвью и определите, какая ветвь имеет самое простое изменение
  2. Примените это простое изменение к версии кода другой ветви, чтобы оно содержало как более простые, так и более сложные изменения
  3. Удалите все секции кода конфликта, кроме той, в которую вы только что слили изменения вместе, в

Альтернатива: разрешить вручную, применяя изменения ветви

Наконец, некоторые конфликты ужасны для понимания даже с diff3. Это происходит, особенно когда diff находит общие линии, которые не являются семантически общими (например, обе ветви имели пустую строку в одном и том же месте!). Например, одна ветвь изменяет отступ тела класса или изменяет порядок похожих методов. В этих случаях лучшей стратегией разрешения может быть изучение изменения с любой стороны слияния и ручное применение diff к другому файлу.

Давайте посмотрим, как мы можем разрешить конфликт в сценарии, где объединение origin/feature1, где lib/message.rb конфликтует.

  1. Решите, является ли наша ветвь, извлеченная в данный момент (HEAD, или --ours), или ветвью, которую мы объединяем (origin/feature1, или --theirs), более простым изменением, чтобы применить. Использование diff с тройной точкой (git diff a...b) показывает изменения, которые произошли в b с момента его последнего отклонения от a, или, другими словами, сравнивает общего предка a и b с b.

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. Проверьте более сложную версию файла. Это удалит все маркеры конфликта и будет использовать выбранную вами сторону.

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. С проверкой сложного изменения поднимите различие более простого изменения (см. Шаг 1). Примените каждое изменение из этого diff к конфликтующему файлу.

359 голосов
/ 19 января 2009

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

Причина, по которой ребаз в этом случае лучше, чем слияние, заключается в том, что:

  • вы переписываете свою локальную историю коммитов с одной из главных (а затем повторно применяете свою работу, разрешая тогда любой конфликт)
  • окончательное слияние, безусловно, будет «ускоренной перемоткой вперед», потому что оно будет иметь всю историю коммитов мастера, плюс только ваши изменения для повторного применения.

Я подтверждаю, что правильный рабочий процесс в этом случае (эволюции для общего набора файлов) - это сначала rebase, затем объединение .

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


По этой теме (перебазировать, а затем объединить рабочий процесс) barraponto упоминает в комментариях два интересных сообщения, оба из randyfay.com :

Используя эту технику, ваша работа всегда идет поверх общедоступной ветви, как патч, обновленный до текущей HEAD.

(аналогичная техника существует для базара )

31 голосов
/ 13 мая 2009

В своем рабочем процессе я перебираю как можно больше (и я стараюсь делать это часто. Не позволяя накопленным несоответствиям резко сократить количество и серьезность столкновений между ветвями).

Однако даже в основном процессе, основанном на ребазе, есть место для слияний.

Напомним, что слияние фактически создает узел, у которого есть два родителя. Теперь рассмотрим следующую ситуацию: у меня есть две независимые ветви функций A и B, и теперь я хочу разработать материал для ветви функций C, который зависит как от A, так и от B, в то время как A и B. рассматриваются.

Что я тогда делаю, так это:

  1. Создать (и оформить) ветвь C поверх A.
  2. Объединить его с B

Теперь ветвь C включает в себя изменения как от A, так и от B, и я могу продолжать развивать ее. Если я сделаю какие-либо изменения в A, то я восстановлю график ветвей следующим образом:

  1. создать ветку T на новой вершине A
  2. объединить T с B
  3. перебазировать С на Т
  4. удалить ветку T

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

21 голосов
/ 10 апреля 2009

НЕ ИСПОЛЬЗУЙТЕ git push origin --mirror ПОД ПОЛНОСТЬЮ ЛЮБОГО ОБСТОЯТЕЛЬСТВА.

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

http://twitter.com/dysinger/status/1273652486

14 голосов
/ 26 мая 2009

У меня есть один вопрос после прочтения вашего объяснения: может быть, вы никогда не делали

git checkout master
git pull origin
git checkout my_new_feature

перед выполнением 'git rebase / merge master' в вашей ветви функций?

Поскольку ваша ветка master не будет обновляться автоматически из репозитория вашего друга. Вы должны сделать это с git pull origin. То есть может быть, вы всегда будете перебазировать из неизменной локальной ветки master? А потом наступает время push, вы запускаете репозиторий, в котором есть (локальные) коммиты, которые вы никогда не видели, и, следовательно, push не выполняется.

13 голосов
/ 19 января 2009

В вашей ситуации, я думаю, ваш партнер прав. Что приятно в перебазировании, так это то, что для постороннего ваши изменения выглядят так, как будто все они произошли в чистой последовательности. Это значит

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

Вы можете продолжать отправлять свою частную ветку разработки в удаленный репозиторий для резервного копирования, но другие не должны рассматривать это как "публичную" ветку, так как вы будете перебазировать. Кстати, простая команда для этого - git push --mirror origin.

Статья Упаковка программного обеспечения с использованием Git довольно неплохо объясняет компромиссы между слиянием и перебазированием. Это немного другой контекст, но принципы одинаковы - в основном все зависит от того, являются ли ваши филиалы общедоступными или частными, и как вы планируете интегрировать их в основную линию.

12 голосов
/ 19 января 2009

В любом случае, я следил за своим рабочим процессом в недавней ветке, и когда я попытался объединить его с мастером, все пошло к черту. Было множество конфликтов с вещами, которые не должны были иметь значения. Конфликты просто не имели смысла для меня. Мне потребовался день, чтобы разобраться во всем, и в итоге я вынужден был принудительно передать удаленному мастеру, поскольку мой локальный мастер разрешил все конфликты, но удаленный все еще не был доволен.

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

Я думаю, вам нужно внимательно посмотреть на то, что произошло. Может ли кто-то другой (намеренно или нет) перемотать удаленную главную ветку между созданием локальной ветки и точкой, в которой вы попытались объединить ее с локальной ветвью?

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

7 голосов
/ 25 августа 2011

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

6 голосов
/ 22 марта 2012

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

  • Создание новой ветви B из существующей ветви A

  • Добавить / зафиксировать изменения в ветви B

  • Перебазировать обновления из ветви A

  • Объединить изменения из ветви B в ветку A "

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/

2 голосов
/ 19 января 2009

С Git нет «правильного» рабочего процесса. Используйте все, что плавает на вашей лодке. Однако, если вы постоянно сталкиваетесь с конфликтами при объединении ветвей, возможно, вам следует лучше координировать свои усилия с другими разработчиками? Похоже, вы двое продолжаете редактировать одни и те же файлы. Кроме того, следите за пробелами и подрывными ключевыми словами (то есть «$ Id $» и другими). ​​

...