Merge made by the recursive strategy
Merge made by the recursive strategy
Стратегии слияния в Git: параметры и примеры
Когда работа завершена, протестирована и готова к передаче в основную линию разработки, ваша команда должна выбрать стратегию слияния. В этой статье рассматриваются разные стратегии и объясняется, как работает Atlassian. По прочтении у вас будет достаточно знаний, чтобы выбрать оптимальный для своей команды вариант.
Стратегии слияния в Git
Под слиянием подразумевается объединение двух веток. Git пытается найти общий базовый коммит для двух или более указателей. Есть несколько методов поиска. Они называются «стратегиями слияния». Когда Git находит общий базовый коммит, создается коммит слияния, который объединяет изменения определенных коммитов. С технической точки зрения, коммит слияния — это обычный коммит с двумя родителями.
Рекурсивная стратегия
Эта стратегия работает с двумя указателями HEAD. Она применяется по умолчанию при выполнении команды pull или merge для одной ветки. Рекурсивная стратегия позволяет обнаруживать и обрабатывать слияния с переименованием, но на сегодняшний день не может использовать обнаруженные копии. Эта стратегия применяется по умолчанию при выполнении команд pull или merge для одной ветки.
Решение
Эта стратегия может разрешить только два указателя HEAD с помощью алгоритма трехстороннего слияния. Данная стратегия пытается выявлять неопределенности при перекрестном слиянии и считается в целом безопасной и быстрой.
Стратегия «Осьминог»
Стратегия «Осьминог» применяется по умолчанию для более чем двух голов. Когда передается больше одной ветки, «осьминог» включается автоматически. Если в слиянии есть конфликты, которые нужно разрешать вручную, эта стратегия блокирует попытку слияния. В основном она используется для объединения голов аналогичных функциональных веток.
Стратегия «Наша»
Стратегия «Наша» (Ours) позволяет работать с множеством веток. Выходной результат слияния всегда соответствует указателю HEAD текущей ветки. Эта стратегия называется «Наша», потому что она игнорирует все изменения из «чужих» веток. Ее назначение — объединение истории аналогичных функциональных веток.
Стратегия «Поддерево»
Это разновидность рекурсивной стратегии. При слиянии A и B, если B — дочернее поддерево A, сначала обновляется B, чтобы отразить древовидную структуру A. Кроме того, обновляется родительское дерево, которое является общим для A и B.
Типы стратегий слияния в Git
Явное слияние
Явное слияние — стандартный вариант, который используется по умолчанию. Слияние называется явным, потому что создается новый коммит слияния. При этом меняется история коммита и явно показано, где выполнено слияние. Содержимое коммита слияния также является явным: можно увидеть, какие коммиты были родительскими для коммита слияния. Некоторые разработчики избегают явных слияний, чтобы не перегружать историю проекта.
Скрытое слияние с использованием методов rebase или fast-forward
Склеивание (обычно без явного слияния)
Параметры рекурсивной стратегии слияния в Git
Рассмотренная выше рекурсивная стратегия имеет свой поднабор параметров.
Не следует путать этот параметр со стратегией слияния «Наша» (Ours). Он позволяет автоматически разрешать конфликты путем предпочтения «нашей» (ours) версии. Изменения с «их» (theirs) стороны внедряются автоматически, если в них отсутствуют конфликты.
Параметр theirs, напротив, отдает предпочтение чужому дереву слияния при разрешении конфликтов.
Этот параметр позволяет избежать ошибок слияния на неважных совпадающих линиях. Его лучше использовать, когда ветки, подлежащие слиянию, сильно расходятся.
Набор параметров, которые ориентированы на символы пробела. Любая линия, которая соответствует поднабору передаваемого параметра, будет проигнорирована.
Этот параметр игнорирует переименованные файлы во время слияния.
Этот параметр является вариантом стратегии «Поддерево» (Subtree). В то время как стратегия работает с двумя деревьями и задает способ их совмещения в общем родителе, этот параметр работает с метаданными пути дерева для обеспечения совмещения.
Наша политика слияния в Git
Atlassian настоятельно рекомендует использовать скрытые слияния. Причина простая: явные слияния оставляют следы и контекст в объединяемых функциях. Перед тем как отправлять функциональную ветку на проверку, следует выполнить очистку локальной истории с помощью команды rebase, однако это не меняет нашу политику, а дополняет ее.
Готовы попробовать ветвление?
Ознакомьтесь с этим интерактивным обучающим руководством.
Git merge using recursive strategy and patience option
How to merge a branch with git using patience option provided by the recursive strategy? I’m using git version 1.7.3.1.msysgit.0
Even docs are not consistent and, what’s more, differ from what the actual command outputs.
and further in text:
Output from the command says:
So I tried a couple versions with following results:
It looks as if the option’s not implemented.
2 Answers 2
Trending sort
Trending sort is based off of the default sorting method — by highest score — but it boosts votes that have happened recently, helping to surface more up-to-date answers.
It falls back to sorting by highest score if no posts are trending.
Switch to Trending sort
I’ve just tried this with git version 1.7.4, and the following command line syntax works for me:
Update Aug. 2021, 10 years later: no more «recursive merge»:
With Git 2.34 (Q4 2021), the default merge strategy becomes ORT («Ostensibly Recursive’s Twin»)
81483fe613 :Update error message and code comment
Acked-by: Derrick Stolee
Acked-by: Johannes Schindelin
Signed-off-by: Elijah Newren
There were two locations in the code that referred to ‘merge-recursive’ but which were also applicable to ‘merge-ort’.
Update them to more general wording.
With Git 2.34 (Q4 2021), documentation updates.
merge-strategies now includes in its man page:
With Git 2.34 (Q4 2021), use ort instead of recursive as the default merge strategy.
f5a3c5e637 :Update docs for change of default merge backend
Signed-off-by: Elijah Newren
git rebase now includes in its man page:
git rebase now includes in its man page:
gitfaq now includes in its man page:
By default, when Git does a merge, it uses a strategy called the ort
merge-strategies now includes in its man page:
This is the default merge strategy when pulling or merging one branch.
This strategy can only resolve two heads using a 3-way merge algorithm.
When there is more than one common ancestor that can be used for 3-way merge, it creates a merged tree of the common ancestors and uses that as the reference tree for the 3-way merge.
This has been reported to result in fewer merge conflicts without causing mismerges by tests done on actual merge commits taken from Linux 2.6 kernel development history.
Additionally this strategy can detect and handle merges involving renames.
It does not make use of detected copies.
merge-strategies now includes in its man page:
The ‘ort’ strategy can take the following options:
recursive
This can only resolve two heads using a 3-way merge algorithm.
When there is more than one common ancestor that can be used for 3-way merge, it creates a merged tree of the common ancestors and uses that as the reference tree for the 3-way merge.
This has been reported to result in fewer merge conflicts without causing mismerges by tests done on actual merge commits taken from Linux 2.6 kernel development history.
Additionally this can detect and handle merges involving renames.
It does not make use of detected copies. This was the default strategy for resolving two heads from Git v0.99.9k until v2.33.0.
The ‘recursive’ strategy takes the same options as ‘ort’.
However, there are three additional options that ‘ort’ ignores (not documented above) that are potentially useful with the ‘recursive’ strategy:
patience;;
no-renames;;
Turn off rename detection. This overrides the merge.renames configuration variable.
Original answer (2011)
I see here the patch introducing the patience option to the recursive merge strategy, base on Git1.7.2.2, but I don’t see it in any of the subsequent release notes.
Yet the patience diff algorithm has been presented since 2009, and is detailed here.
The merge command should understand this option. but this function below seems to never be called (not anywhere in merge-recursive.c or in any other *.c file!):
Продвинутое слияние
Обычно выполнять слияния в Git довольно легко. Git упрощает повторные слияния с одной и той же веткой, таким образом, позволяя вам иметь очень долго живущую ветку, и вы можете сохранять ее всё это время в актуальном состоянии, часто разрешая маленькие конфликты, а не доводить дело до одного большого конфликта по завершению всех изменений.
Однако, иногда всё же будут возникать сложные конфликты. В отличие от других систем управления версиями, Git не пытается быть слишком умным при разрешении конфликтов слияния. Философия Git заключается в том, чтобы быть умным, когда слияние разрешается однозначно, но если возникает конфликт, он не пытается сумничать и разрешить его автоматически. Поэтому, если вы слишком долго откладываете слияние двух быстрорастущих веток, вы можете столкнуться с некоторыми проблемами.
В этом разделе мы рассмотрим некоторые из возможных проблем и инструменты, которые предоставляет Git, чтобы помочь вам справиться с этими более сложными ситуациями. Мы также рассмотрим некоторые другие нестандартные типы слияний, которые вы можете выполнять, и вы узнаете как можно откатить уже выполненные слияния.
Конфликты слияния
Мы рассказали некоторые основы разрешения конфликтов слияния в Основные конфликты слияния, для работы с более сложными конфликтами Git предоставляет несколько инструментов, которые помогут вам понять, что произошло и как лучше обойтись с конфликтом.
Во-первых, если есть возможность, перед слиянием, в котором может возникнуть конфликт, позаботьтесь о том, чтобы ваша рабочая копия была без локальных изменений. Если у вас есть несохранённые наработки, либо припрячьте их, либо сохраните их во временной ветке. Таким образом, вы сможете легко отменить любые изменения, которые сделаете в рабочем каталоге. Если при выполнении слияния вы не сохраните сделанные изменения, то некоторые из описанных ниже приёмов могут привести к утрате этих наработок.
Давайте рассмотрим очень простой пример. Допустим, у нас есть файл с исходниками на Ruby, выводящими на экран строку ‘hello world’.
В нашем репозитории, мы создадим новую ветку по имени whitespace и выполним замену всех окончаний строк в стиле Unix на окончания строк в стиле DOS. Фактически, изменения будут внесены в каждую строку, но изменятся только пробельные символы. Затем мы заменим строку «hello world» на «hello mundo».
Теперь мы переключимся обратно на ветку master и добавим к функции некоторую документацию.
Теперь мы попытаемся слить в текущую ветку whitespace и в результате получим конфликты, так как изменились пробельные символы.
Прерывание слияния
Эта команда пытается откатить ваше состояние до того, что было до запуска слияния. Завершиться неудачно она может только в случаях, если перед запуском слияния у вас были не припрятанные или не зафиксированные изменения в рабочем каталоге, во всех остальных случаях всё будет хорошо.
Игнорирование пробельных символов
В данном конкретном случае конфликты связаны с пробельными символами. Мы знаем это, так как это простой пример, но в реальных ситуациях это также легко определить при изучении конфликта, так как каждая строка в нем будет удалена и добавлена снова. По умолчанию Git считает все эти строки изменёнными и поэтому не может слить файлы.
Поскольку в этом примере реальные изменения файлов не конфликтуют, то при игнорировании изменений пробельных символов всё сольётся хорошо.
Это значительно облегчает жизнь, если кто-то в вашей команде любит временами заменять все пробелы на табуляции или наоборот.
Ручное слияние файлов
Хотя Git довольно хорошо обрабатывает пробельные символы, с другими типами изменений он не может справиться автоматически, но существуют другие варианты исправления. Например, представим, что Git не умеет обрабатывать изменения пробельных символов и нам нужно сделать это вручную.
Во-первых, мы перейдём в состояние конфликта слияния. Затем нам необходимо получить копии нашей версии файла, их версии файла (из ветки, которую мы сливаем) и общей версии (от которой ответвились первые две). Затем мы исправим либо их версию, либо нашу и повторим слияние только для этого файла.
Получить эти три версии файла, на самом деле, довольно легко. Git хранит все эти версии в индексе в разных «состояниях», каждое из которых имеет ассоциированный с ним номер. Состояние 1 — это общий предок, состояние 2 — ваша версия и состояния 3 взято из MERGE_HEAD — версия, которую вы сливаете («их» версия).
Вы можете извлечь копию каждой из этих версий конфликтующего файла с помощью команды git show и специального синтаксиса.
Выражение :1:hello.rb является просто сокращением для поиска такого SHA-1 хеша.
Итак, здесь мы можем легко увидеть что же произошло с нашей веткой, какие изменения в действительности внесло слияние в данный файл — изменение только одной строки.
В данный момент мы можем использовать команду git clean для того, чтобы удалить не нужные более дополнительные файлы, созданные нами для выполнения слияния.
Использование команды checkout в конфликтах
Возможно, нас по каким-то причинам не устраивает необходимость выполнения слияния в текущий момент, или мы не можем хорошо исправить конфликт и нам необходимо больше информации.
Давайте немного изменим пример. Предположим, что у нас есть две долгоживущих ветки, каждая из которых имеет несколько коммитов, что при слиянии приводит к справедливому конфликту.
Мы хотели бы увидеть в чем состоит данный конфликт. Если мы откроем конфликтующий файл, то увидим нечто подобное:
В обеих сливаемых ветках в этот файл было добавлено содержимое, но в некоторых коммитах изменялись одни и те же строки, что и привело к конфликту.
Давайте рассмотрим несколько находящихся в вашем распоряжении инструментов, которые позволяют определить как возник этот конфликт. Возможно, не понятно как именно вы должны исправить конфликт и вам требуется больше информации.
После того, как вы выполните эту команду, файл будет выглядеть так:
Это может быть действительно полезным при возникновении конфликтов в бинарных файлах (в этом случае вы можете просто выбрать одну из версий), или при необходимости слить из другой ветки только некоторые файлы (в этом случае вы можете выполнить слияние, а затем перед коммитом переключить нужные файлы на требуемые версии).
История при слиянии
Для получения полного списка всех уникальных коммитов, которые были сделаны в любой из сливаемых веток, мы можем использовать синтаксис «трёх точек», который мы изучили в Три точки.
Это список всех шести коммитов, включённых в слияние, с указанием также ветки разработки, в которой находится каждый из коммитов.
Комбинированный формат изменений
Так как Git добавляет в индекс все успешные результаты слияния, то при вызове git diff в состоянии конфликта слияния будет отображено только то, что сейчас конфликтует. Это может быть полезно, так как вы сможете увидеть какие ещё конфликты нужно разрешить.
Если вы выполните git diff сразу после конфликта слияния, то получите информацию в довольно своеобразном формате.
Такой формат называется «комбинированным» («Combined Diff»), для каждого различия в нем содержится два раздела с информацией. В первом разделе отображены различия строки (добавлена она или удалена) между «вашей» веткой и содержимым вашего рабочего каталога, а во втором разделе содержится то же самое, но между «их» веткой и рабочим каталогом.
Таким образом, в данном примере вы можете увидеть строки и >>>>>>> в файле в вашем рабочем каталоге, хотя они отсутствовали в сливаемых ветках. Это вполне оправдано, потому что, добавляя их, инструмент слияния предоставляет вам дополнительную информацию, но предполагается, что мы удалим их.
В этом выводе указано, что строка «hola world» при слиянии присутствовала в «нашей» ветке, но отсутствовала в рабочей копии, строка «hello mundo» была в «их» ветке, но не в рабочей копии, и, наконец, «hola mundo» не была ни в одной из сливаемых веток, но сейчас присутствует в рабочей копии. Это бывает полезно просмотреть перед коммитом разрешения конфликта.
Отмена слияний
Теперь когда вы знаете как создать коммит слияния, вы можете сделать его по ошибке. Одна из замечательных вещей в работе с Git — это то, что ошибки совершать не страшно, так как есть возможность исправить их (и в большинстве случаев сделать это просто).
Есть два подхода к решению этой проблемы, в зависимости от того, какой результат вы хотите получить.
Исправление ссылок
, то указатели веток восстановятся так, что будут выглядеть следующим образом:
Перемещает ветку, на которую указывает HEAD. В данном случае мы хотим переместить master туда, где она была до коммита слияния ( C6 ).
Приводит индекс к такому же виду что и HEAD.
Приводит рабочий каталог к такому же виду, что и индекс.
Отмена коммита
Если перемещение указателей ветки вам не подходит, Git предоставляет возможность сделать новый коммит, который откатывает все изменения, сделанные в другом. Git называет эту операцию «восстановлением» («revert»), в данном примере вы можете вызвать её следующим образом:
История с коммитом восстановления (отменой коммита слияния) выглядит следующим образом:
Лучшим решением данной проблемы является откат коммита отмены слияния, так как теперь вы хотите внести изменения, которые были отменены, а затем создание нового коммита слияния:
Другие типы слияний
До этого момента мы рассматривали типичные слияния двух веток, которые обычно выполняются с использованием стратегии слияния, называемой «рекурсивной». Но существуют и другие типы слияния веток. Давайте кратко рассмотрим некоторые из них.
Выбор «нашей» или «их» версий
В этом случае Git не будет добавлять маркеры конфликта. Все неконфликтующие изменения он сольёт, а для конфликтующих он целиком возьмёт ту версию, которую вы указали (это относится и к бинарным файлам).
Если мы вернёмся к примеру «hello world», который использовали раньше, то увидим, что попытка слияния в нашу ветку приведёт к конфликту.
В этом случае, вместо добавления в файл маркеров конфликта с «hello mundo» в качестве одной версии и с «hola world» в качестве другой, Git просто выберет «hola world». Однако, все другие неконфликтующие изменения будут слиты успешно.
На случай если вам нужно нечто подобное, но вы хотите, чтобы Git даже не пытался сливать изменения из другой версии, существует более суровый вариант — стратегия слияния «ours». Важно отметить, что это не то же самое что опция «ours» рекурсивной стратегии слияния.
Фактически, эта стратегия выполнит ненастоящее слияние. Она создаст новый коммит слияния, у которой родителями будут обе ветки, но при этом данная стратегия даже не взглянет на ветку, которую вы сливаете. В качестве результата слияния она просто оставляет тот код, который находится в вашей текущей ветке.
Вы можете видеть, что между веткой, в которой мы были, и результатом слияния нет никаких отличий.
Слияние поддеревьев
Идея слияния поддеревьев состоит в том, что у вас есть два проекта и один из проектов отображается в подкаталог другого. Когда вы выполняете слияние поддеревьев, Git в большинстве случаев способен понять, что одно из них является поддеревом другого и выполнить слияние подходящим способом.
Далее мы рассмотрим пример добавления в существующий проект другого проекта и последующее слияние кода второго проекта в подкаталог первого.
Первым делом мы добавим в наш проект приложение Rack. Мы добавим Rack в наш собственный проект, как удалённый репозиторий, а затем выгрузим его в отдельную ветку.
Таким образом, теперь у нас в ветке rack_branch находится основная ветка проекта Rack, а в ветке master — наш собственный проект. Если вы переключитесь сначала на одну ветку, а затем на другую, то увидите, что они имеют абсолютно разное содержимое:
Может показаться странным, но, на самом деле, ветки в вашем репозитории не обязаны быть ветками одного проекта. Это мало распространено, так как редко бывает полезным, но иметь ветки, имеющие абсолютно разные истории, довольно легко.
Когда мы будем выполнять коммит, он будет выглядеть так, как будто все файлы проекта Rack были добавлены в этот подкаталог — например, мы скопировали их из архива. Важно отметить, что слить изменения одной из веток в другую довольно легко. Таким образом, если проект Rack обновился, мы можем получить изменения из его репозитория просто переключившись на соответствующую ветку и выполнив операцию git pull :
Таким образом, слияние поддеревьев даёт нам возможность использовать рабочий процесс в некоторой степени похожий на рабочий процесс с подмодулями, но при этом без использования подмодулей (которые мы рассмотрим в Подмодули). Мы можем держать ветки с другими связанными проектами в нашем репозитории и периодически сливать их как поддеревья в наш проект. С одной стороны это удобно, например, тем, что весь код хранится в одном месте. Однако, при этом есть и некоторые недостатки — поддеревья немного сложнее, проще допустить ошибки при повторной интеграции изменений или случайно отправить ветку не в тот репозиторий.
Вот как выглядит процедура сравнения содержимого подкаталога rack с содержимым ветки master на сервере после последнего скачивания изменений:
(Git Merging) When to use ‘ours’ strategy, ‘ours’ option and ‘theirs’ option?
Definition of recursive merge strategy pulled from the git merge documentation.
This can only resolve two heads using a 3-way merge algorithm. When there is more than one common ancestor that can be used for 3-way merge, it creates a merged tree of the common ancestors and uses that as the reference tree for the 3-way merge. This has been reported to result in fewer merge conflicts without causing mismerges by tests done on actual merge commits taken from Linux 2.6 kernel development history. Additionally this can detect and handle merges involving renames. This is the default merge strategy when pulling or merging one branch.
The recursive strategy can take the following options:
As stated, the recursive strategy, which is the default strategy, makes use of the 3-way recursive merge algorithm (explained here and on Wikipedia).
My understanding is that conflicting hunks must be manually resolved and they are typically represented like this
The ours option of the recursive merge strategy is documented as follows:
This option forces conflicting hunks to be auto-resolved cleanly by favoring our version. Changes from the other tree that do not conflict with our side are reflected to the merge result. For a binary file, the entire contents are taken from our side.
This should not be confused with the ours merge strategy, which does not even look at what the other tree contains at all. It discards everything the other tree did, declaring our history contains all that happened in it.
Now suppose I have the heads of two branches Y and M, with a common base ancestor B as follows
When merging Y and M using the default recursive strategy, line 30 will become Print(«hello»); since at line 30, Y represents a change from the base ancestor and M does not. But if I were on branch M and run
will line 30 become Print(«bye»); in the merged output?
To those of you who say that this is obvious, note that the ours option states
This option forces conflicting hunks to be auto-resolved cleanly by favoring our version.
But (as I understand) there is no conflicting hunk at line 30.
For completeness, I will also give the documentation of the theirs option:
The documentation for the ours strategy is as follows:
So returning to the example above, if I ran
on branch M, it is clear that line 30 will be Print(«bye»); in the merged output. In this case, why is there also not a theirs strategy? How can I achieve behaviour equal and opposite to the ours strategy?
I’m asking because I’m working on a project where I want to regularly and completely overwrite the master branch with changes from another development branch, whenever the code on the development branch builds successfully. This way I can ensure that my development branch never drifts too far off from the master branch and also that the code on the master branch will build successfully.
I have seen this question which recommends the solution
My current solution is to do
I have also seen this question which recommends using the theirs option of the recursive strategy. But as the ours option of the recursive strategy is clearly different ours strategy, so too would the theirs option of the recursive strategy be different from the theirs strategy that I’m talking about.
Внутреннее устройство Git: хранение данных и merge
О статье
Материал ориентирован прежде всего на читателей, умеющих работать с Git на уровне обычного пользователя и знающих основные концепции работы с ним. Возможно, статья не будет содержать ничего нового для разработчиков систем контроля версий, поддерживающих легкое создание веток и их надежное слияние. Вся информация взята из открытых источников, в том числе из исходных текстов Git (2d242fb3fc19fc9ba046accdd9210be8b9913f64).
Хранение данных: объекты
Узнать тип объекта можно, набрав
Также хранение объектов целиком позволяет делать надежное слияние веток с разрешением конфликтов. Но об этом чуть позже.
Tree (иерархия ФС)
объекте типа дерево (англ. tree) хранится список записей, который соответствует иерархии файловой системы. Одна запись представляет из себя следующее:
Права файла в Git могут иметь лишь очень ограниченный набор значений:
040000 — директория;
100644 — обычный файл;
100755 — файл с правами исполнения;
120000 — символическая ссылка.
Тип объекта — это BLOB или tree, для файла и директории соответственно. То есть в объекте типа tree для корневой директории хранится вся иерархия файловой системы, поскольку внутри одного дерева могут быть ссылки на другие деревья.
Commit
В Git один коммит (англ. сommit) представляет из себя ссылку на объект tree, соответствующий корневой директории, и ссылку на родительский коммит (кроме самого первого коммита в репозитории). Также в коммите есть информация об авторе и UNIX timestamp от времени создания.
Pack-файлы
«. It’s worth explaining (you are probably aware of it, but
let me go through the basics anyway) how git delta-chains work, and how
they are so different from most other systems.
In other SCM’s, a delta-chain is generally fixed. It might be «forwards»
or «backwards», and it might evolve a bit as you work with the repository,
but generally it’s a chain of changes to a single file represented as some
kind of single SCM entity. In CVS, it’s obviously the *,v file, and a lot
of other systems do rather similar things.
Git also does delta-chains, but it does them a lot more «loosely». There
is no fixed entity. Delta’s are generated against any random other version
that git deems to be a good delta candidate (with various fairly
successful heursitics), and there are absolutely no hard grouping rules.
It also means that the choice of deltas is a much more open-ended
question. If you limit the delta chain to just one file, you really don’t
have a lot of choices on what to do about deltas, but in git, it really
can be a totally different issue».
Если кратко, то в pack-файлах объекты группируются по схожести (например, тип и размер), после чего они сохраняются в виде «цепочек». Первый элемент цепочки представляет из себя самую новую версию объекта, а следующий за ним являются диффом к предыдущему. Самые новые версии объекта считаются наиболее запрашиваемыми, поэтому они хранятся выше в цепочке.
Таким образом, Git всё же хранит диффы, но только на уровне непосредственного хранения данных. С точки зрения любого API уровнем выше, Git оперирует объектами целиком, что позволяет реализовывать различные стратегии слияния и легко разрешать конфликты.
Хранение истории
В Git нет отдельного хранилища истории. Всю историю можно развернуть, но лишь пройдя по ссылкам на родителя из нужного вам коммита. Если необходимо просмотреть историю только по одному файлу (или по поддиректории), Git всё равно должен проделать то же самое, но он будет возвращать отфильтрованные результаты. Стоит иметь это ввиду, когда вы делаете интеграцию с Git, и не заставлять Git делать полный просмотр истории на каждый файл.
К тому же, как вы могли заметить, Git не хранит информацию о переименовании файлов. Если нужно понять, переименован файл или нет, Git производит анализ содержимого хранящихся у него объектов и с некоторым (настраиваемым) допуском считает, что файл был переименован.
Merge: трехстороннее слияние (стратегия resolve)
Если нужно выполнить слияние двух веток, то git по умолчанию использует стратегию recursive, но о ней чуть позже. До того, как появилась эта стратегия, использовалась стратегия resolve, которая представляет из себя трехстороннее слияние. Для того, чтобы выполнить такое слияние, нужно иметь 3 версии: общий родитель, версия из одной ветки и версия из другой ветки. Если вы выполняете слияние файлов, то такое трехсторонее слияние может выполняться утилитой diff3, которая входит в стандартный пакет diffutils. Эта скромная и редко упоминаемая утилита, так или иначе, делает всю «грязную работу» по слиянию в большинстве существующих систем контроля версий, включая RCS, CVS, SVN и, конечно же, Git.
Помимо использования аналога diff3 (конкретная реализация, используемая в Git — это LibXDiff), Git также «на лету» вычисляет переименования файлов и использует эту информацию для слияния tree-объектов. Слияние иерархий директорий не представляет из себя ничего принципиально сложного по сравнению с тем, чтобы выполнить слияние файлов, но порождает очень много различных видов конфликтов.
Небольшая иллюстрация того, как Git выполняет трехстороннее слияние в простом случае (взято из man git-merge):
Предположим, у нас есть такая история и текущая ветка — master:
Тогда git merge topic повторит изменения, сделанные в topic начиная с коммита, когда история разветвилась (коммит E), и создаст новый коммит H, у которого будет два родителя, и сообщение коммита, которое предоставит пользователь.
Тем не менее разработка в ветках topic и master может быть продолжена, и тогда слияние уже не будет выглядеть так просто: у нас может быть больше, чем один коммит, который подходит под определение «общий предок»:
Если мы будем использовать стратегию resolve, то будет выбран самый старый общий предок (коммит E). Если в результате выполнения merge были конфликты, разрешённые в коммите H, нам всё равно нужно будет разрешать их ещё раз.
Для выполнения слияния с помощью стратегии resolve Git возьмет коммит E в качестве общего предка и коммиты M и P в качестве двух новых версий. Если в коммите C был конфликт, то конфликтующие изменения можно откатить с помощью git revert (например, это проделано в коммите K), тогда конечное состояние M уже не будет содержать в себе конфликта, и при слиянии конфликтов тоже не будет.
Merge made by the ‘recursive’ strategy
Представим себе такую историю:
Результатом выполнения этой операции будет виртуальный коммит, который представляет из себя «смерженное» состояние всех общих предков в правильном порядке — разрешение конфликтов тоже попадет в этот коммит, причем более свежие коммиты будут иметь приоритет. Когда мы получили общего предка, выполняется трехсторонее слияние, описанное выше.
Выдержка из merge-recursive.c:
Низкоуровневые команды Git
Если вы работали какое-то время с Git, то вы наверняка знаете о командах checkout, branch, pull, push, rebase, commit и некоторых других. Но изначально Git создавался не как полноценная система контроля версий, а как фреймворк для её создания. Поэтому в Git есть очень богатый набор встроенных команд, которые работают на низком уровне. Приведем некоторые из них, весьма полезные, на наш взгляд:
git rev-parse
Эта команда является очень простой: она возвращает хеш коммита для указанной ревизии. Например, git rev-parse HEAD вернет хеш коммита, на который указывает HEAD.
Подводные камни: Что касается запросов вида branch ^other_branch, Git может неправильно вывести результаты, если у коммитов стоит неправильное время. Например, в выводе могут быть пропущены коммиты, которые «произошли в будущем» по сравнению с merge ветки.
git diff-index
Показывает разницу между рабочей копией и индексом (.git/index). В индексе Git хранит кеш lstat() от всех файлов, о которых он знает.
git cat-file
Эта команда уже встречалась в статье, но её всё же стоит упомянуть ещё раз. Она позволяет получить содержимое коммита и любого другого объекта Git.
git ls-tree
Выводит содержимое tree-объекта в приемлемом виде.
git ls-remote
Выводит информацию о ветках и тегах (вместе с хешами коммитов) из указанного удаленного репозитория.
GIT_SSH
Если вы писали скрипты, которые делают git pull, то скорее всего сталкивались с тем, что SSH запрашивает подтверждение «аутентичности» удаленного репозитория, причём делает это интерактивно. Решение этой проблемы не столь изящное, потому что GIT_SSH должен быть путем к исполняемому файлу (а не опции SSH):
Заключение
Как можно было увидеть, Git позволяет действительно хорошо и надежно работать с ветками, в том числе корректно обрабатывать ситуации, когда у двух веток есть более одного общего предка. Если только вашей целью не является написание своей системы контроля версий, мы бы рекомендовали использовать результаты работы Git, а не пытаться воспроизвести его алгоритм слияния.
Надеемся, данный материал оказался интересным для вас и позволил понять, почему Git работает именно так, а не иначе. Полагаем, что статья будет также полезной для разработчиков различных интерфейсов к Git, приводя к более глубокому пониманию того, что же происходит «под капотом».
Юрий Насретдинов, разработчик Badoo
Источники информации:
- http://stackoverflow.com/questions/5024402/git-merge-using-recursive-strategy-and-patience-option
- http://git-scm.com/book/ru/v2/%D0%98%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B-Git-%D0%9F%D1%80%D0%BE%D0%B4%D0%B2%D0%B8%D0%BD%D1%83%D1%82%D0%BE%D0%B5-%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D0%B5
- http://stackoverflow.com/questions/45402742/git-merging-when-to-use-ours-strategy-ours-option-and-theirs-option
- http://habr.com/ru/company/badoo/blog/163853/