Чем отличается merge от commit?
Чем отличается merge от commit? Когда комитим — заливаем, скажем, в главную ветку свои изменения. Когда мержим — объединяем свою копию с изменениями в главной ветке. Когда нужно выложить проект в продакшн, переключаемся на trunk, мержим, а потом еще и комитим, зачем второе действие?
Отслеживать
задан 16 июл 2012 в 9:56
1,936 7 7 золотых знаков 28 28 серебряных знаков 47 47 бронзовых знаков
@nMike, все очень просто. «Meржим» всегда свою копию, база не меняется. «Коммитим» — это заливаем свое в базу. Насколько помню, если при commit возникает конфликт, то commit не проходит. Надо сделать svn merge в локальную копию, разрешить конфликты, а потом закинуть в базу svn ci .
16 июл 2012 в 11:00
тогда чем отличается merge от update?
1 авг 2012 в 3:25
@nMike А чем отличается зеленый от сладкого?
1 авг 2012 в 11:14
Мержим из транка, апдейтим из ветки. Если работаем в транке, то операции эквивалентны.
29 авг 2012 в 3:44
@nMike вы ошибаетесь, merge от update отличаются не только этим.
22 дек 2016 в 6:24
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Если бы merge был бы эквивалентен merge + commit , то пользователь потерял бы возможность выполнять следующие действия:
- Целиком review’ить изменения в feature branch , которые появляются после merge —reintegrate .
- Решать conflict’ы и tree conflict’ы , которые возникли в результате операции merge .
Отслеживать
ответ дан 16 июл 2012 в 10:06
M. Williams M. Williams
23.5k 1 1 золотой знак 41 41 серебряный знак 58 58 бронзовых знаков
Дело в том, что commit — это не просто заливка изменений. Сущность этой операции заключается в том, что изменения, сделанные локально, преобразуются в изменения, которые могут быть успешно добавлены в репозиторий и зафиксированы в нём в виде очередной ревизии. При этом локальные изменения всегда выражены в виде различий относительно последней ревизии, о которой известно локально (т.е. ревизии, которая была во время последнего update), а в репозиторий могут быть добавлены лишь такие изменения, которые выражены в виде различий относительно последней ревизии самого репозитория.
В простейшем случае, когда в репозиторий не было добавлено ничего со стороны, преобразование изменений сводится к тому, чтобы не делать ничего, поскольку изменения и так уже выражены в виде различий относительно последней ревизии в репозитории. Поэтому операция commit становится тривиальной и выглядит, как простая заливка изменений. В случае же, когда с момента последнего update в репозитории уже появилось что-то новое, попытка сделать тривиальный commit проваливается и приходится проходить всю процедуру полностью, т.е. сначала приспосабливать свои локальные изменения к тем другим изменениям, которые уже были зафиксированы в репозитории кем-то другим, и только потом заливать их и фиксировать.
Когда мы заливаем в репозиторий изменения, сделанные в рабочей копии, необходимость делать commit в самом конце очевидна. Ведь в репозитории ещё нет наших локальных изменений, поскольку предыдущая попытка сделать тривиальный commit провалилась. Не так очевидна эта необходимость в ситуации, когда мы объединяем одну ветку с другой прямо в репозитории с помощью merge. Дело тут вот в чём. На самом деле, операция commit играет ещё и роль подтверждения. Заливка в репозиторий новых изменений как в тривиальном случае, так и после адаптации всегда выполняется после того, как пользователь увидел окончательный вариант, поэтому его подтверждение, необходимое для фиксации очередной ревизии, как бы подразумевается и даётся тем же самым commit’ом. А вот про объединение веток в репозитории такого сказать нельзя. Результат объединения веток обычно отличается от того, что было в последних ревизиях как одной ветки, так и другой. Он представляет собой совершенно новую ревизию. И даже в том случае, когда участие пользователя в самом объединении веток не требуется, от него всё равно требуется получить подтверждение после того, как будет получен окончательный вариант новой ревизии. Именно для этого и нужен commit после merge. Если объединение прошло на автомате, то commit играет лишь роль подтверждения, которое необходимо для фиксации любой ревизии в репозитории.
Merge commit что это
В прошлой статье мы создали новую ветку newbranch. Сейчас мы ее дополним и сделаем слияние с основной веткой master
Что такое merge?
Merge — это процесс объединения изменений из одной ветки Git с другой веткой. При выполнении операции merge Git сравнивает две ветки и создает новый коммит, содержащий все изменения из обеих веток. Этот коммит объединяет истории изменений двух веток и становится новой точкой слияния веток.
Merge может быть выполнен между любыми двумя ветками в Git, но наиболее распространенный случай — объединение изменений из ветки feature (функциональности) в ветку master (основная ветка). Это позволяет добавлять новые функции в основную ветку и сохранять историю изменений в отдельной ветке, не нарушая работу основной ветки.
Изменим нашу вторую ветку new.
Я создам третий файл txt под названием file-in-new-branch.txt и заполню его фразой file in new branch.
Вот так выглядит наш проект сейчас (ветка new):

Ещё давайте изменим first.txt и second.txt дописав в них фразы commit in new branch

Отлично, мы создали новый файл(file-in-new-branch.txt) в новой ветке и имзенили два предыдущих. Чтобы изменения записались в репозиторий выполним уже изученные комманды:
git add . git commit -m 'Commit-in-new-branch'
Результат:

Теперь давайте перейдем обратно в ветку master командой:
git checkout master
И если посмотреть теперь на файлы нашего проекта, то мы не увидим файла** file-in-new-branch.txt** и изменений в первых двух файлах. Они не пропали и не удалились, а остались в ветке new. К ним так же можно обрано вернуться: git checkout new

Megre веток
Для выполнения операции слияния веток необходимо находиться в той ветке, которую вы желаете объединить с другой веткой. В нашем случае мы должны находиться в ветке master для слияния с new.
git merge new

Ветки объединилсь и теперь всё что находилось в ветке new — есть и в основной ветке master. После слияния веток, обычно удаляют дополнительную ветку:
git branch -d new
Текущую ветку (в которой находитесь сейчас) удалить нельзя!

Мы произвели слияние второй ветки с главной, после чего удалили её.
В следующем уроке поговорим, как загрузить проект на удаленный репозиторий на Git-hub
Меню категорий
-
Загрузка категорий.
Добро пожаловать в Блог Разработчика Владислава Александровича.
Ведется медленная, но уверенная разработка функционала сайта.
Django Core: 0.3.4 / Next.js 1.0 / UPD: 05.06.2023
7.8 Инструменты Git — Продвинутое слияние
Обычно выполнять слияния в Git довольно легко. Git упрощает повторные слияния с одной и той же веткой, таким образом, позволяя вам иметь очень долго живущую ветку, и вы можете сохранять её всё это время в актуальном состоянии, часто разрешая маленькие конфликты, а не доводить дело до одного большого конфликта по завершению всех изменений.
Однако, иногда всё же будут возникать сложные конфликты. В отличие от других систем управления версиями, Git не пытается быть слишком умным при разрешении конфликтов слияния. Философия Git заключается в том, чтобы быть умным, когда слияние разрешается однозначно, но если возникает конфликт, он не пытается сумничать и разрешить его автоматически. Поэтому, если вы слишком долго откладываете слияние двух быстрорастущих веток, вы можете столкнуться с некоторыми проблемами.
В этом разделе мы рассмотрим некоторые из возможных проблем и инструменты, которые предоставляет Git, чтобы помочь вам справиться с этими более сложными ситуациями. Мы также рассмотрим некоторые другие нестандартные типы слияний, которые вы можете выполнять, и вы узнаете как можно откатить уже выполненные слияния.
Конфликты слияния
Мы рассказали некоторые основы разрешения конфликтов слияния в Основные конфликты слияния, для работы с более сложными конфликтами Git предоставляет несколько инструментов, которые помогут вам понять, что произошло и как лучше обойтись с конфликтом.
Во-первых, если есть возможность, перед слиянием, в котором может возникнуть конфликт, позаботьтесь о том, чтобы ваша рабочая копия была без локальных изменений. Если у вас есть несохранённые наработки, либо припрячьте их, либо сохраните их во временной ветке. Таким образом, вы сможете легко отменить любые изменения, которые сделаете в рабочем каталоге. Если при выполнении слияния вы не сохраните сделанные изменения, то некоторые из описанных ниже приёмов могут привести к утрате этих наработок.
Давайте рассмотрим очень простой пример. Допустим, у нас есть файл с исходниками на Ruby, выводящими на экран строку ‘hello world’.
#! /usr/bin/env ruby def hello puts 'hello world' end hello()
В нашем репозитории, мы создадим новую ветку по имени whitespace и выполним замену всех окончаний строк в стиле Unix на окончания строк в стиле DOS. Фактически, изменения будут внесены в каждую строку, но изменятся только пробельные символы. Затем мы заменим строку «hello world» на «hello mundo».
$ git checkout -b whitespace Switched to a new branch 'whitespace' $ unix2dos hello.rb unix2dos: converting file hello.rb to DOS format . $ git commit -am 'Convert hello.rb to DOS' [whitespace 3270f76] Convert hello.rb to DOS 1 file changed, 7 insertions(+), 7 deletions(-) $ vim hello.rb $ git diff -b diff --git a/hello.rb b/hello.rb index ac51efd..e85207e 100755 --- a/hello.rb +++ b/hello.rb @@ -1,7 +1,7 @@ #! /usr/bin/env ruby def hello - puts 'hello world' + puts 'hello mundo'^M end hello() $ git commit -am 'Use Spanish instead of English' [whitespace 6d338d2] Use Spanish instead of English 1 file changed, 1 insertion(+), 1 deletion(-)
Теперь мы переключимся обратно на ветку master и добавим к функции некоторую документацию.
$ git checkout master Switched to branch 'master' $ vim hello.rb $ git diff diff --git a/hello.rb b/hello.rb index ac51efd..36c06c8 100755 --- a/hello.rb +++ b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby +# prints out a greeting def hello puts 'hello world' end $ git commit -am 'Add comment documenting the function' [master bec6336] Add comment documenting the function 1 file changed, 1 insertion(+)
Теперь мы попытаемся слить в текущую ветку whitespace и в результате получим конфликты, так как изменились пробельные символы.
$ git merge whitespace Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
Прерывание слияния
В данный момент у нас есть несколько вариантов дальнейших действий. Во-первых, давайте рассмотрим как выйти из этой ситуации. Если вы, возможно, не были готовы к конфликтам и на самом деле не хотите связываться с ними, вы можете просто отменить попытку слияния, используя команду git merge —abort .
$ git status -sb ## master UU hello.rb $ git merge --abort $ git status -sb ## master
Эта команда пытается откатить ваше состояние до того, что было до запуска слияния. Завершиться неудачно она может только в случаях, если перед запуском слияния у вас были не припрятанные или не зафиксированные изменения в рабочем каталоге, во всех остальных случаях всё будет хорошо.
Если по каким-то причинам вы обнаружили себя в ужасном состоянии и хотите просто начать всё сначала, вы можете также выполнить git reset —hard HEAD (либо вместо HEAD указав то, куда вы хотите откатиться). Но помните, что это откатит все изменения в рабочем каталоге, поэтому удостоверьтесь, что никакие из них вам не нужны.
Игнорирование пробельных символов
В данном конкретном случае конфликты связаны с пробельными символами. Мы знаем это, так как это простой пример, но в реальных ситуациях это также легко определить при изучении конфликта, так как каждая строка в нём будет удалена и добавлена снова. По умолчанию Git считает все эти строки изменёнными и поэтому не может слить файлы.
Стратегии слияния, используемой по умолчанию, можно передать аргументы, и некоторые из них предназначены для соответствующей настройки игнорирования изменений пробельных символов. Если вы видите, что множество конфликтов слияния вызваны пробельными символами, то вы можете прервать слияние и запустить его снова, но на этот раз с опцией -Xignore-all-space или -Xignore-space-change . Первая опция игнорирует изменения в любом количестве существующих пробельных символов, вторая игнорирует вообще все изменения пробельных символов.
$ git merge -Xignore-space-change whitespace Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
Поскольку в этом примере реальные изменения файлов не конфликтуют, то при игнорировании изменений пробельных символов всё сольётся хорошо.
Это значительно облегчает жизнь, если кто-то в вашей команде любит временами заменять все пробелы на табуляции или наоборот.
Ручное слияние файлов
Хотя Git довольно хорошо обрабатывает пробельные символы, с другими типами изменений он не может справиться автоматически, но существуют другие варианты исправления. Например, представим, что Git не умеет обрабатывать изменения пробельных символов и нам нужно сделать это вручную.
То что нам действительно нужно — это перед выполнением самого слияния прогнать сливаемый файл через программу dos2unix . Как мы будем делать это?
Во-первых, мы перейдём в состояние конфликта слияния. Затем нам необходимо получить копии нашей версии файла, их версии файла (из ветки, которую мы сливаем) и общей версии (от которой ответвились первые две). Затем мы исправим либо их версию, либо нашу и повторим слияние только для этого файла.
Получить эти три версии файла, на самом деле, довольно легко. Git хранит все эти версии в индексе в разных «состояниях», каждое из которых имеет ассоциированный с ним номер. Состояние 1 — это общий предок, состояние 2 — ваша версия и состояния 3 взято из MERGE_HEAD — версия, которую вы сливаете («их» версия).
Вы можете извлечь копию каждой из этих версий конфликтующего файла с помощью команды git show и специального синтаксиса.
$ git show :1:hello.rb > hello.common.rb $ git show :2:hello.rb > hello.ours.rb $ git show :3:hello.rb > hello.theirs.rb
Если вы хотите что-то более суровое, то можете также воспользоваться служебной командой ls-files -u для получения SHA-1 хешей для каждого из этих файлов.
$ git ls-files -u 100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1 hello.rb 100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2 hello.rb 100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3 hello.rb
Выражение :1:hello.rb является просто сокращением для поиска такого SHA-1 хеша.
Теперь, когда в нашем рабочем каталоге присутствует содержимое всех трёх состояний, мы можем вручную исправить их, чтобы устранить проблемы с пробельными символами и повторно выполнить слияние с помощью малоизвестной команды git merge-file , которая делает именно это.
$ dos2unix hello.theirs.rb dos2unix: converting file hello.theirs.rb to Unix format . $ git merge-file -p \ hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb $ git diff -b diff --cc hello.rb index 36c06c8,e85207e..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,8 -1,7 +1,8 @@@ #! /usr/bin/env ruby +# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
Теперь у нас есть корректно слитый файл. На самом деле, данный способ лучше, чем использование опции ignore-all-space , так как в его рамках вместо игнорирования изменений пробельных символов перед слиянием выполняется корректное исправление таких изменений. При слиянии с ignore-all-space мы в результате получим несколько строк с окончаниями в стиле DOS, то есть в одном файле смешаются разные стили окончания строк.
Если перед коммитом изменений вы хотите посмотреть какие в действительности были различия между состояниями, то можете воспользоваться командой git diff , сравнивающей содержимое вашего рабочего каталога, которое будет зафиксировано как результат слияния, с любым из трёх состояний. Давайте посмотрим на все эти сравнения.
Чтобы сравнить результат слияния с тем, что было в вашей ветке до слияния, или другими словами увидеть, что привнесло данное слияние, вы можете выполнить git diff —ours
$ git diff --ours * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index 36c06c8..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -2,7 +2,7 @@ # prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
Итак, здесь мы можем легко увидеть что же произошло с нашей веткой, какие изменения в действительности внесло слияние в данный файл — изменение только одной строки.
Если вы хотите узнать чем результат слияния отличается от сливаемой ветки, то можете выполнить команду git diff —theirs . В этом и следующем примере мы используем опцию -w для того, чтобы не учитывать изменения в пробельных символах, так как мы сравниваем результат с тем, что есть в Git, а не с нашим исправленным файлом hello.theirs.rb .
$ git diff --theirs -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index e85207e..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby +# prints out a greeting def hello puts 'hello mundo' end
И, наконец, вы можете узнать как изменился файл по сравнению сразу с обеими ветками с помощью команды git diff —base .
$ git diff --base -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index ac51efd..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -1,7 +1,8 @@ #! /usr/bin/env ruby +# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
В данный момент мы можем использовать команду git clean для того, чтобы удалить не нужные более дополнительные файлы, созданные нами для выполнения слияния.
$ git clean -f Removing hello.common.rb Removing hello.ours.rb Removing hello.theirs.rb
Использование команды checkout в конфликтах
Возможно, нас по каким-то причинам не устраивает необходимость выполнения слияния в текущий момент, или мы не можем хорошо исправить конфликт и нам необходимо больше информации.
Давайте немного изменим пример. Предположим, что у нас есть две долгоживущих ветки, каждая из которых имеет несколько коммитов, что при слиянии приводит к справедливому конфликту.
$ git log --graph --oneline --decorate --all * f1270f7 (HEAD, master) Update README * 9af9d3b Create README * 694971d Update phrase to 'hola world' | * e3eb223 (mundo) Add more tests | * 7cff591 Create initial testing script | * c3ffff1 Change text to 'hello mundo' |/ * b7dcc89 Initial hello world code
У нас есть три уникальных коммита, которые присутствуют только в ветке master и три других, которые присутствуют в ветке mundo . Если мы попытаемся слить ветку mundo , то получим конфликт.
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
Мы хотели бы увидеть в чем состоит данный конфликт. Если мы откроем конфликтующий файл, то увидим нечто подобное:
#! /usr/bin/env ruby def hello >>>>>> mundo end hello()
В обеих сливаемых ветках в этот файл было добавлено содержимое, но в некоторых коммитах изменялись одни и те же строки, что и привело к конфликту.
Давайте рассмотрим несколько находящихся в вашем распоряжении инструментов, которые позволяют определить как возник этот конфликт. Возможно, не понятно как именно вы должны исправить конфликт и вам требуется больше информации.
Полезным в данном случае инструментом является команда git checkout с опцией —conflict . Она заново выкачает файл и заменит маркеры конфликта. Это может быть полезно, если вы хотите восстановить маркеры конфликта и попробовать разрешить его снова.
В качестве значения опции —conflict вы можете указывать diff3 или merge (последнее значение используется по умолчанию). Если вы укажете diff3 , Git будет использовать немного другую версию маркеров конфликта — помимо «нашей» и «их» версий файлов будет также отображена «базовая» версия, и таким образом вы получите больше информации.
$ git checkout --conflict=diff3 hello.rb
После того, как вы выполните эту команду, файл будет выглядеть так:
#! /usr/bin/env ruby def hello >>>>>> theirs end hello()
Если вам нравится такой формат вывода, то вы можете использовать его по умолчанию для будущих конфликтов слияния, установив параметру merge.conflictstyle значение diff3 .
$ git config --global merge.conflictstyle diff3
Команде git checkout также можно передать опции —ours и —theirs , которые позволяют действительно быстро выбрать одну из версий файлов, не выполняя слияния совсем.
Это может быть действительно полезным при возникновении конфликтов в бинарных файлах (в этом случае вы можете просто выбрать одну из версий), или при необходимости слить из другой ветки только некоторые файлы (в этом случае вы можете выполнить слияние, а затем перед коммитом переключить нужные файлы на требуемые версии).
История при слиянии
Другой полезный инструмент при разрешении конфликтов слияния — это команда git log . Она поможет вам получить информацию о том, что могло привести к возникновению конфликтов. Временами может быть очень полезным просмотреть историю, чтобы понять почему в двух ветках разработки изменялась одна и та же область кода.
Для получения полного списка всех уникальных коммитов, которые были сделаны в любой из сливаемых веток, мы можем использовать синтаксис «трёх точек», который мы изучили в Три точки.
$ git log --oneline --left-right HEAD. MERGE_HEAD < f1270f7 Update README < 9af9d3b Create README < 694971d Update phrase to 'hola world' >e3eb223 Add more tests > 7cff591 Create initial testing script > c3ffff1 Change text to 'hello mundo'
Это список всех шести коммитов, включённых в слияние, с указанием также ветки разработки, в которой находится каждый из коммитов.
Мы также можем сократить его, попросив предоставить нам более специализированную информацию. Если мы добавим опцию —merge к команде git log , то она покажет нам только те коммиты, в которых изменялся конфликтующий в данный момент файл.
$ git log --oneline --left-right --merge < 694971d Update phrase to 'hola world' >c3ffff1 Change text to 'hello mundo'
Если вы выполните эту команду с опцией -p , то получите только список изменений файла, на котором возник конфликт. Это может быть действительно полезным для быстрого получения информации, которая необходима, чтобы понять почему что-либо конфликтует и как наиболее правильно это разрешить.
Комбинированный формат изменений
Так как Git добавляет в индекс все успешные результаты слияния, то при вызове git diff в состоянии конфликта слияния будет отображено только то, что сейчас конфликтует. Это может быть полезно, так как вы сможете увидеть какие ещё конфликты нужно разрешить.
Если вы выполните git diff сразу после конфликта слияния, то получите информацию в довольно своеобразном формате.
$ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,11 @@@ #! /usr/bin/env ruby def hello ++>>>>>> mundo end hello()
Такой формат называется «комбинированным» («Combined Diff»), для каждого различия в нём содержится два раздела с информацией. В первом разделе отображены различия строки (добавлена она или удалена) между «вашей» веткой и содержимым вашего рабочего каталога, а во втором разделе содержится то же самое, но между «их» веткой и рабочим каталогом.
Таким образом, в данном примере вы можете увидеть строки >>>>>> в файле в вашем рабочем каталоге, хотя они отсутствовали в сливаемых ветках. Это вполне оправдано, потому что, добавляя их, инструмент слияния предоставляет вам дополнительную информацию, но предполагается, что мы удалим их.
Если мы разрешим конфликт и снова выполним команду git diff , то получим ту же информацию, но в немного более полезном представлении.
$ vim hello.rb $ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end hello()
В этом выводе указано, что строка «hola world» при слиянии присутствовала в «нашей» ветке, но отсутствовала в рабочей копии, строка «hello mundo» была в «их» ветке, но не в рабочей копии, и, наконец, «hola mundo» не была ни в одной из сливаемых веток, но сейчас присутствует в рабочей копии. Это бывает полезно просмотреть перед коммитом разрешения конфликта.
Такую же информацию вы можете получить и после выполнения слияния с помощью команды git log , узнав таким образом как был разрешён конфликт. Git выводит информацию в таком формате, если вы выполните git show для коммита слияния или вызовете команду git log -p с опцией —cc (без неё данная команда не показывает изменения для коммитов слияния).
$ git log --cc -p -1 commit 14f41939956d80b9e17bb8721354c33f8d5b5a79 Merge: f1270f7 e3eb223 Author: Scott Chacon Date: Fri Sep 19 18:14:49 2014 +0200 Merge branch 'mundo' Conflicts: hello.rb diff --cc hello.rb index 0399cd5,59727f0..e1d0799 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end hello()
Отмена слияний
Теперь когда вы знаете как создать коммит слияния, вы можете сделать его по ошибке. Одна из замечательных вещей в работе с Git — это то, что ошибки совершать не страшно, так как есть возможность исправить их (и в большинстве случаев сделать это просто).
Коммит слияния не исключение. Допустим, вы начали работать в тематической ветке, случайно слили её в master , и теперь ваша история коммитов выглядит следующим образом:

Рисунок 137. Случайный коммит слияния
Есть два подхода к решению этой проблемы, в зависимости от того, какой результат вы хотите получить.
Исправление ссылок
Если нежелаемый коммит слияния существует только в вашем локальном репозитории, то простейшее и лучшее решение состоит в перемещении веток так, чтобы они указывали туда куда вам нужно. В большинстве случаев, если вы после случайного git merge выполните команду git reset —hard HEAD~ , то указатели веток восстановятся так, что будут выглядеть следующим образом:

Рисунок 138. История после git reset —hard HEAD~
Мы рассматривали команду reset ранее в Раскрытие тайн reset, поэтому вам должно быть не сложно разобраться с тем, что здесь происходит. Здесь небольшое напоминание: reset —hard обычно выполняет три шага:
- Перемещает ветку, на которую указывает HEAD. В данном случае мы хотим переместить master туда, где она была до коммита слияния ( C6 ).
- Приводит индекс к такому же виду что и HEAD.
- Приводит рабочий каталог к такому же виду, что и индекс.
Недостаток этого подхода состоит в изменении истории, что может привести к проблемам в случае совместно используемого репозитория. Загляните в Опасности перемещения, чтобы узнать что именно может произойти; кратко говоря, если у других людей уже есть какие-то из изменяемых вами коммитов, вы должны отказаться от использования reset . Этот подход также не будет работать, если после слияния уже был сделан хотя бы один коммит; перемещение ссылки фактически приведёт к потере этих изменений.
Отмена коммита
Если перемещение указателей ветки вам не подходит, Git предоставляет возможность сделать новый коммит, который откатывает все изменения, сделанные в другом. Git называет эту операцию «восстановлением» («revert»), в данном примере вы можете вызвать её следующим образом:
$ git revert -m 1 HEAD [master b1d8379] Revert "Merge branch 'topic'"
Опция -m 1 указывает какой родитель является «основной веткой» и должен быть сохранен. Когда вы выполняете слияние в HEAD ( git merge topic ), новый коммит будет иметь двух родителей: первый из них HEAD ( C6 ), а второй — вершина ветки, которую сливают с текущей ( C4 ). В данном случае, мы хотим отменить все изменения, внесённые слиянием родителя #2 ( C4 ), и сохранить при этом всё содержимое из родителя #1 ( C6 ).
История с коммитом восстановления (отменой коммита слияния) выглядит следующим образом:

Рисунок 139. История после git revert -m 1
Новый коммит ^M имеет точно такое же содержимое как C6 , таким образом, начиная с неё всё выглядит так, как будто слияние никогда не выполнялось, за тем лишь исключением, что «теперь уже не слитые» коммиты всё также присутствуют в истории HEAD . Git придёт в замешательство, если вы вновь попытаетесь слить topic в ветку master :
$ git merge topic Already up-to-date.
В ветке topic нет ничего, что ещё недоступно из ветки master . Плохо, что в случае добавления новых наработок в topic , при повторении слияния Git добавит только те изменения, которые были сделаны после отмены слияния:

Рисунок 140. История с плохим слиянием
Лучшим решением данной проблемы является откат коммита отмены слияния, так как теперь вы хотите внести изменения, которые были отменены, а затем создание нового коммита слияния:
$ git revert ^M [master 09f0126] Revert "Revert "Merge branch 'topic'"" $ git merge topic

Рисунок 141. История после повторения отменённого слияния
В этом примере, M и ^M отменены. В коммите ^^M , фактически, сливаются изменения из C3 и C4 , а в C8 — изменения из C7 , таким образом, ветка topic полностью слита.
Другие типы слияний
До этого момента мы рассматривали типичные слияния двух веток, которые обычно выполняются с использованием стратегии слияния, называемой «рекурсивной». Но существуют и другие типы слияния веток. Давайте кратко рассмотрим некоторые из них.
Выбор «нашей» или «их» версий
Во-первых, существует ещё один полезный приём, который мы можем использовать в обычном «рекурсивном» режиме слияния. Мы уже видели опции ignore-all-space и ignore-space-change , которые передаются с префиксом -X , но мы можем также попросить Git при возникновении конфликта использовать ту или иную версию файлов.
По умолчанию, когда Git при слиянии веток замечает конфликт, он добавляет в код маркеры конфликта, отмечает файл как конфликтующий и позволяет вам разрешить его. Если же вместо ручного разрешения конфликта вы хотите, чтобы Git просто использовал какую-то определённую версию файла, а другую игнорировал, то вы можете передать команде merge одну из двух опций -Xours или -Xtheirs .
В этом случае Git не будет добавлять маркеры конфликта. Все неконфликтующие изменения он сольёт, а для конфликтующих он целиком возьмёт ту версию, которую вы указали (это относится и к бинарным файлам).
Если мы вернёмся к примеру «hello world», который использовали раньше, то увидим, что попытка слияния в нашу ветку приведёт к конфликту.
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Resolved 'hello.rb' using previous resolution. Automatic merge failed; fix conflicts and then commit the result.
Однако, если мы выполним слияние с опцией -Xours или -Xtheirs , конфликта не будет.
$ git merge -Xours mundo Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 +- test.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 test.sh
В этом случае, вместо добавления в файл маркеров конфликта с «hello mundo» в качестве одной версии и с «hola world» в качестве другой, Git просто выберет «hola world». Однако, все другие неконфликтующие изменения будут слиты успешно.
Такая же опция может быть передана команде git merge-file , которую мы обсуждали ранее, то есть для слияния отдельных файлов можно использовать команду git merge-file —ours .
На случай если вам нужно нечто подобное, но вы хотите, чтобы Git даже не пытался сливать изменения из другой версии, существует более суровый вариант — стратегия слияния «ours». Важно отметить, что это не то же самое что опция «ours» рекурсивной стратегии слияния.
Фактически, эта стратегия выполнит ненастоящее слияние. Она создаст новый коммит слияния, у которого родителями будут обе ветки, но при этом данная стратегия даже не взглянет на ветку, которую вы сливаете. В качестве результата слияния она просто оставляет тот код, который находится в вашей текущей ветке.
$ git merge -s ours mundo Merge made by the 'ours' strategy. $ git diff HEAD HEAD~ $
Вы можете видеть, что между веткой, в которой мы были, и результатом слияния нет никаких отличий.
Это часто бывает полезно, когда нужно заставить Git считать, что ветка уже слита, а реальное слияние отложить на потом. Для примера предположим, что вы создали ветку release и проделали в ней некоторую работу, которую когда-то впоследствии захотите слить обратно в master . Тем временем в master были сделаны некоторые исправления, которые необходимо перенести также в вашу ветку release . Вы можете слить ветку с исправлениями в release , а затем выполнить merge -s ours этой ветки в master (хотя исправления в ней уже присутствуют), так что позже, когда вы будете снова сливать ветку release , не возникнет конфликтов, связанных с этими исправлениями.
Слияние поддеревьев
Идея слияния поддеревьев состоит в том, что у вас есть два проекта и один из проектов отображается в подкаталог другого. Когда вы выполняете слияние поддеревьев, Git в большинстве случаев способен понять, что одно из них является поддеревом другого и выполнить слияние подходящим способом.
Далее мы рассмотрим пример добавления в существующий проект другого проекта и последующее слияние кода второго проекта в подкаталог первого.
Первым делом мы добавим в наш проект приложение Rack. Мы добавим Rack в наш собственный проект, как удалённый репозиторий, а затем выгрузим его в отдельную ветку.
$ git remote add rack_remote https://github.com/rack/rack $ git fetch rack_remote --no-tags warning: no common commits remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done. Resolving deltas: 100% (1952/1952), done. From https://github.com/rack/rack * [new branch] build -> rack_remote/build * [new branch] master -> rack_remote/master * [new branch] rack-0.4 -> rack_remote/rack-0.4 * [new branch] rack-0.9 -> rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch"
Таким образом, теперь у нас в ветке rack_branch находится основная ветка проекта Rack, а в ветке master — наш собственный проект. Если вы переключитесь сначала на одну ветку, а затем на другую, то увидите, что они имеют абсолютно разное содержимое:
$ ls AUTHORS KNOWN-ISSUES Rakefile contrib lib COPYING README bin example test $ git checkout master Switched to branch "master" $ ls README
Может показаться странным, но, на самом деле, ветки в вашем репозитории не обязаны быть ветками одного проекта. Это мало распространено, так как редко бывает полезным, но иметь ветки, имеющие абсолютно разные истории, довольно легко.
В данном примере, мы хотим выгрузить проект Rack в подкаталог нашего основного проекта. В Git мы можем выполнить это с помощью команды git read-tree . Вы узнаете больше о команде read-tree и её друзьях в главе Git изнутри, сейчас же вам достаточно знать, что она считывает содержимое некоторой ветки в ваш текущий индекс и рабочий каталог. Мы просто переключимся обратно на ветку master и выгрузим ветку rack_branch в подкаталог rack ветки master нашего основного проекта:
$ git read-tree --prefix=rack/ -u rack_branch
Когда мы будем выполнять коммит, он будет выглядеть так, как будто все файлы проекта Rack были добавлены в этот подкаталог — например, мы скопировали их из архива. Важно отметить, что слить изменения одной из веток в другую довольно легко. Таким образом, если проект Rack обновился, мы можем получить изменения из его репозитория просто переключившись на соответствующую ветку и выполнив операцию git pull :
$ git checkout rack_branch $ git pull
Затем мы можем слить эти изменения обратно в нашу ветку master .
Для того, чтобы получить изменения и заполнить сообщение коммита используйте параметр —squash , вместе с опцией -Xsubtree рекурсивной стратегии слияния. Вообще-то, по умолчанию используется именно рекурсивная стратегия слияния, но мы указали и её тоже для пущей ясности.
$ git checkout master $ git merge --squash -s recursive -Xsubtree=rack rack_branch Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested
Все изменения из проекта Rack слиты и подготовлены для локального коммита. Вы также можете поступить наоборот — сделать изменения в подкаталоге rack вашей основной ветки и затем слить их в вашу ветку rack_branch , чтобы позже передать их ответственным за проекты или отправить их в вышестоящий репозиторий проекта Rack.
Таким образом, слияние поддеревьев даёт нам возможность использовать рабочий процесс в некоторой степени похожий на рабочий процесс с подмодулями, но при этом без использования подмодулей (которые мы рассмотрим в Подмодули). Мы можем держать ветки с другими связанными проектами в нашем репозитории и периодически сливать их как поддеревья в наш проект. С одной стороны это удобно, например, тем, что весь код хранится в одном месте. Однако, при этом есть и некоторые недостатки — поддеревья немного сложнее, проще допустить ошибки при повторной интеграции изменений или случайно отправить ветку не в тот репозиторий.
Другая небольшая странность состоит в том, что для получения различий между содержимым подкаталога rack и содержимого ветки rack_branch — для того, чтобы увидеть необходимо ли выполнять слияния между ними — вы не можете использовать обычную команду diff . Вместо этого следует выполнить команду git diff-tree , указав ветку, с которой производится сравнение:
$ git diff-tree -p rack_branch
Вот как выглядит процедура сравнения содержимого подкаталога rack с содержимым ветки master на сервере после последнего скачивания изменений:
$ git diff-tree -p rack_remote/master
merge vs rebase
Когда над проектом работает несколько человек, в какой-то момент нужно объединить код. Команды rebase и merge имеют разные подходы для консолидации изменений из одной ветви в другую.

git merge
В git merge используется неразрушающая операция для объединения историй двух веток без их изменения. Команда создает новый merge commit . Это отличный способ консолидировать изменения, однако ваша история коммитов может получить несколько merge-коммитов в зависимости от того, насколько активна мастер-ветка.
Чтобы объединить последние изменения из master в вашу ветку:
git checkout [branch-name] git merge master
git rebase
Команда git rebase перемещает историю всей ветки поверх другой, переписывая историю проекта новыми коммитами.
Команда полезна, если вы предпочитаете чистую и линейную историю проекта. Однако перестроить изменения, перенесенные в главную ветку удаленного репозитория небезопасно, т. к. вы будете изменять историю главной ветви, в то время как члены вашей команды будут продолжать работать над ней.
Кроме того, Git не позволит вам легко запушить пересобранный бранч на удаленный репозиторий. Вам придется заставить его это сделать при помощи git push —force , который перезаписывает удаленную ветку и может вызвать проблемы у других участников.
Заключение
Git очень мощный инструмент, имеющий массу интересных и малоизвестных команд. В этой публикации мы рассмотрели команды, которые чаще всего вызывают бурю эмоций и комментариев на просторах сети. Внимательно изучите данные команды, и если что-то останется непонятным – обратитесь к официальному хелпу .