Как и / или почему слияние в Git лучше, чем в SVN?

В некоторых местах я слышал, что одна из главных причин, почему распределенные системы контроля версий сияют, объединение гораздо лучше, чем в традиционных инструментах, таких как SVN. На самом ли деле это связано с внутренними различиями в работе этих двух систем, или у конкретных реализаций DVCS, таких как Git/Mercurial, просто более умные алгоритмы слияния, чем у SVN?

7 ответов

Решение

Утверждение о том, что слияние лучше в DVCS, чем в Subversion, было в значительной степени основано на том, как ветвление и слияние работали в Subversion некоторое время назад. Subversion до 1.5.0 не хранил никакой информации о том, когда ветви были объединены, поэтому, когда вы хотели объединить, вы должны были указать, какой диапазон ревизий нужно объединить.

Так почему же слияния Subversion отстой?

Обдумайте этот пример:

      1   2   4     6     8
trunk o-->o-->o---->o---->o
       \
        \   3     5     7
b1       +->o---->o---->o

Когда мы хотим объединить изменения b1 в транк, мы выполним следующую команду, стоя на папке, для которой выделен транк:

svn merge -r 2:7 {link to branch b1}

... который попытается объединить изменения из b1 в ваш локальный рабочий каталог. И затем вы фиксируете изменения после разрешения любых конфликтов и проверки результата. Когда вы фиксируете дерево ревизий, это будет выглядеть так:

      1   2   4     6     8   9
trunk o-->o-->o---->o---->o-->o      "the merge commit is at r9"
       \
        \   3     5     7
b1       +->o---->o---->o

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

           12        14
trunk  …-->o-------->o
                                     "Okay, so when did we merge last time?"
              13        15
b1     …----->o-------->o

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

Для смягчения этой Subversion теперь хранятся метаданные для ветвления и слияния. Это решило бы все проблемы правильно?

И, кстати, Subversion все еще отстой…

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

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

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

Так почему же DVCS, такие как Git, Mercurial и Bazaar, лучше, чем Subversion при ветвлении и слиянии?

Причина этого очень проста: ветвление - это первоклассная концепция. По своему дизайну нет виртуальных каталогов, а ветки - это жесткие объекты в DVCS, которые должны быть такими, чтобы они работали просто с синхронизацией репозиториев (т. Е. Push и pull).

Первое, что вы делаете, когда работаете с DVCS, это клонируете репозитории (git's clone, хг clone и бзр branch). Концептуально клонирование - это то же самое, что создание ветки в управлении версиями. Некоторые называют это разветвлением или ветвлением (хотя последнее часто также используется для обозначения совмещенных ветвей), но это одно и то же. Каждый пользователь запускает свой собственный репозиторий, что означает, что у вас есть ветвление для каждого пользователя.

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

Очень простой пример слияния был бы таким; представьте себе центральное хранилище под названием origin и пользователь, Алиса, клонирует хранилище на свою машину.

         a…   b…   c…
origin   o<---o<---o
                   ^master
         |
         | clone
         v

         a…   b…   c…
alice    o<---o<---o
                   ^master
                   ^origin/master

Что происходит во время клонирования, так это то, что каждая ревизия копируется в Алису в точности так, как она была (что подтверждается уникально идентифицируемыми хэш-идентификаторами), и отмечает, где находятся ветви источника.

Затем Алиса работает над своим репо, фиксируя в своем собственном репозитории и решает выдвинуть свои изменения:

         a…   b…   c…
origin   o<---o<---o
                   ^ master

              "what'll happen after a push?"


         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                   ^origin/master

Решение довольно простое, единственное, что origin репозиторий должен сделать, это взять все новые ревизии и переместить его ветку в самую новую ревизию (которую git называет "перемотка вперед"):

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                             ^origin/master

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

Так как насчет того, чтобы показать мне пример с настоящим слиянием?

Следует признать, что приведенный выше пример очень прост, поэтому давайте сделаем гораздо более скрученный, хотя и более распространенный пример. Помни что origin начал с трех ревизий? Ну, парень, который сделал их, давайте назовем его Боб, работал сам и сделал коммит в своем собственном репозитории:

         a…   b…   c…   f…
bob      o<---o<---o<---o
                        ^ master
                   ^ origin/master

                   "can Bob push his changes?" 

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

Теперь Боб не может перенести свои изменения прямо в origin репозиторий. Как система обнаруживает это, проверяя, происходят ли ревизии Боба непосредственно от origin Это не так. Любая попытка толкнуть приведет к тому, что система скажет что-то похожее на " Э-э... Боюсь, я не могу позволить тебе сделать это, Боб ".

Так что Боб должен вставить и затем объединить изменения (с pull; или ртуть pull а также merge; или BZR merge). Это двухступенчатый процесс. Сначала Боб должен получить новые ревизии, которые будут копировать их из origin репозиторий. Теперь мы можем видеть, что график расходится:

                        v master
         a…   b…   c…   f…
bob      o<---o<---o<---o
                   ^
                   |    d…   e…
                   +----o<---o
                             ^ origin/master

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

Второй шаг процесса извлечения - объединить расходящиеся подсказки и зафиксировать результат:

                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master

Надеемся, что слияние не приведет к конфликтам (если вы предвидите их, вы можете выполнить два шага вручную в git с помощью fetch а также merge). Что нужно сделать позже, это снова подтолкнуть эти изменения к origin, что приведет к ускоренному слиянию, поскольку коммит слияния является прямым потомком последнего из origin репозиторий:

                                 v origin/master
                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

                                 v master
         a…   b…   c…   f…       1…
origin   o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

Есть еще одна опция для слияния в git и hg, называемая rebase, которая будет перемещать изменения Боба после последних изменений. Поскольку я не хочу, чтобы этот ответ был более подробным, я позволю вам прочитать об этом документацию git, mercurial или bazaar.

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

Существует также проблема отправки исправлений между каждым разработчиком, что было огромной проблемой в Subversion, которая смягчается в git, hg и bzr уникальными идентифицируемыми ревизиями. После того, как кто-то слил свои изменения (т.е. сделал коммит слияния) и отправил его всем остальным в команде для потребления, либо отправив в центральный репозиторий, либо отправив патчи, им не нужно беспокоиться о слиянии, потому что это уже произошло, Мартин Фаулер называет этот способ беспорядочной интеграции.

Поскольку структура отличается от Subversion, вместо этого используется группа обеспечения доступности баз данных, что позволяет выполнять ветвление и объединение более простым способом не только для системы, но и для пользователя.

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

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

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

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

Кроме того, SVN утверждает, что отслеживает переименования, чтобы помочь слиянию перемещенных файлов. Но на самом деле он все еще сохраняет их как копию и отдельное действие удаления, и алгоритм слияния все еще сталкивается с ними в ситуациях изменения / переименования, то есть когда файл изменяется в одной ветви и переименовывается в другой, и эти ветви быть объединенным. Такие ситуации по-прежнему приводят к ложным конфликтам слияния, а в случае переименования каталогов это даже приводит к потере изменений без вывода сообщений. (Люди из SVN, как правило, указывают на то, что изменения все еще в истории, но это мало помогает, когда они не находятся в результате слияния, где они должны появиться.

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

Представление слияния SVN также имеет проблемы; в 1.5/1.6 вы можете выполнять слияние с транка на ветку так часто, как вам нравится, автоматически, но слияние в другом направлении необходимо было объявить (--reintegrate) и оставил ветку в неработоспособном состоянии. Намного позже они узнали, что это на самом деле не так, и что а) --reintegrate можно вычислить автоматически, и б) возможны повторные слияния в обоих направлениях.

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

Другие замечания, сделанные в ответах, такие как принудительная глобальная видимость ветвей в SVN, не имеют отношения к возможностям слияния (но для удобства использования). Кроме того, "Git хранит изменения, в то время как SVN хранит (что-то другое)", в основном неуместно. Git концептуально сохраняет каждый коммит в отдельном дереве (например, в файле tar), а затем использует довольно некоторую эвристику для его эффективного хранения. Вычисление изменений между двумя коммитами выполняется отдельно от реализации хранилища. Что действительно верно, так это то, что Git хранит историю DAG в гораздо более простой форме, чем SVN, которая делает mergeinfo. Любой, кто пытается понять последнее, поймет, о чем я.

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

Я прочитал принятый ответ. Это просто неправильно.

Слияние SVN может быть болезненным, а также может быть громоздким. Но игнорируйте, как это на самом деле работает в течение минуты. Нет информации, которую Git хранит или может получить, которую SVN также не хранит или не может получить. Что еще более важно, нет никаких причин, почему хранение отдельных (иногда частичных) копий системы контроля версий предоставит вам более актуальную информацию. Две структуры полностью эквивалентны.

Предположим, вы хотите сделать "какую-то умную вещь", Git "лучше умеет". А ты вещь проверена в SVN.

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

В конце концов, любая система контроля версий позволит мне

1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.

Кроме того, для объединения также полезно (или важно) знать

3. The set of changes have been merged into a given branch/revision.

Mercurial, Git и Subversion (теперь изначально изначально использовавшие svnmerge.py) могут предоставить все три элемента информации. Чтобы продемонстрировать что-то принципиально лучшее с DVC, укажите четвертую часть информации, которая доступна в Git/Mercurial/DVC, недоступной в SVN / централизованном VC.

Это не значит, что они не лучшие инструменты!

Одна вещь, которая не была упомянута в других ответах и ​​которая действительно является большим преимуществом DVCS, - это то, что вы можете зафиксировать локально, прежде чем вносить изменения. В SVN, когда у меня были какие-то изменения, я хотел зарегистрироваться, и кто-то уже сделал коммит в той же ветке, это означало, что мне нужно было сделать svn update прежде чем я смог совершить. Это означает, что мои изменения и изменения от другого человека теперь смешаны вместе, и нет способа прервать слияние (как с git reset или же hg update -C), потому что нет обязательства вернуться к. Если слияние нетривиально, это означает, что вы не можете продолжать работу над своей функцией, пока не очистите результат слияния.

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

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

Я до сих пор активно использую SVN, но я очень доволен тем, как немного раз я использовал Git.

Приятно читать, если у вас есть время: почему я выбрал Git

Просто прочитайте статью в блоге Джоэла (к сожалению, его последнюю). Это о Mercurial, но на самом деле говорится о преимуществах распределенных VC-систем, таких как Git.

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

Прочитайте статью здесь.

Другие вопросы по тегам