Слияние: Hg/Git против SVN
Я часто читаю, что Hg (и Git и...) лучше сливаются, чем SVN, но я никогда не видел практических примеров того, как Hg/Git может слить что-то, где SVN выходит из строя (или где SVN требует ручного вмешательства). Не могли бы вы опубликовать несколько пошаговых списков операций ветвления / изменения / фиксации /...-, которые показывают, где SVN потерпит неудачу, в то время как Hg/Git счастливо движется? Практические, не исключительные случаи, пожалуйста...
Немного предыстории: у нас есть несколько десятков разработчиков, работающих над проектами, использующими SVN, с каждым проектом (или группой похожих проектов) в своем собственном репозитории. Мы знаем, как применять ветки релизов и функций, чтобы мы не сталкивались с проблемами очень часто (то есть, мы были там, но мы научились преодолевать проблемы Джоэла "одного программиста, причиняющего травму всей команде"). или "требуется шесть разработчиков на две недели для реинтеграции филиала"). У нас есть ветки релизов, которые очень стабильны и используются только для исправления ошибок. У нас есть транки, которые должны быть достаточно стабильными, чтобы иметь возможность создавать релиз в течение одной недели. И у нас есть тематические ветви, над которыми могут работать отдельные разработчики или группы разработчиков. Да, они удаляются после реинтеграции, поэтому они не загромождают хранилище.;)
Поэтому я все еще пытаюсь найти преимущества Hg/Git перед SVN. Я хотел бы получить практический опыт, но пока нет более крупных проектов, которые мы могли бы перевести на Hg/Git, поэтому я застрял в игре с небольшими искусственными проектами, которые содержат только несколько составленных файлов. И я ищу несколько случаев, когда вы можете почувствовать впечатляющую силу Hg/Git, поскольку до сих пор я часто читал о них, но сам не смог их найти.
6 ответов
Сам я не использую Subversion, но из примечаний к выпуску Subversion 1.5: отслеживание слияний (основополагающее) похоже, что есть следующие отличия от того, как работает отслеживание слияний в системах контроля версий с полной DAG, таких как Git или Mercurial.
Слияние транка с веткой отличается от слияния ветки с транком: по некоторым причинам слияние транка с веткой требует
--reintegrate
возможностьsvn merge
,В распределенных системах контроля версий, таких как Git или Mercurial, нет никакой технической разницы между стволом и ветвью: все ветви созданы равными (хотя может быть социальная разница). Слияние в любом направлении выполняется одинаково.
Вы должны предоставить новый
-g
(--use-merge-history
) вариантsvn log
а такжеsvn blame
принять во внимание отслеживание слияния.В Git и Mercurial отслеживание слияний автоматически учитывается при отображении истории (журнала) и вины. В Git вы можете просить следовать за первым родителем только с
--first-parent
(Я полагаю, аналогичная опция существует и для Mercurial), чтобы "отбросить" информацию отслеживания слияний вgit log
,Из того, что я понимаю
svn:mergeinfo
свойство хранит информацию о конфликтах по каждому пути (Subversion основана на наборах изменений), тогда как в Git и Mercurial это просто коммит объектов, которые могут иметь более одного родителя.Подраздел "Известные проблемы" для отслеживания слияния в Subversion предполагает, что повторное / циклическое / отражающее слияние может работать неправильно. Это означает, что в следующих историях второе слияние может не сработать ("A" может быть стволом или ответвлением, а "B" может быть ответвлением или стволом соответственно):
* --- * --- x --- * --- y --- * --- *---*---M2 <- A \ \ / - * ---- M1 --- * --- * --- / <- B
В случае, если вышеприведенный ASCII-art нарушается: ветвь "B" создается (разветвляется) из ветки "A" при ревизии "x", затем более поздняя ветвь "A" объединяется при ревизии "y" в ветку "B" как слияние 'M1', и, наконец, ветвь 'B' объединяется с ветвью 'A' как слияние 'M2'.
* --- * --- x --- * ----- M1 - *---*---M2 <- A \ / / \ - * --- у --- * --- * --- / <- B
В случае, если вышеприведенный ASCII-арт нарушается: ветвь "B" создается (разветвляется) из ветки "A" при ревизии "x", она объединяется с ветвью "A" в "y" как "M1", а затем снова объединены в ветвь "А" как "М2".
Subversion может не поддерживать расширенный случай слияния.
* --- ----- б B1- M1 -*---M3 \ \ / / \ ИКС / \ / \ / \- В2- М2- *
Git отлично справляется с этой ситуацией на практике, используя "рекурсивную" стратегию слияния. Я не уверен насчет Mercurial.
В "Известных проблемах" есть предупреждение, что отслеживание слияний может не работать с переименованием файлов, например, когда одна сторона переименовывает файл (и, возможно, изменяет его), а вторая сторона изменяет файл без переименования (под старым именем).
И Git, и Mercurial на практике справляются с таким случаем: Git использует обнаружение переименования, Mercurial использует отслеживание переименования.
НТН
Я тоже искал случай, когда, скажем, Subversion не может слить ветку и Mercurial (а Git, Bazaar, ...) делает правильные вещи.
Книга SVN описывает, как переименованные файлы объединяются неправильно. Это относится к Subversion 1.5, 1.6, 1.7 и 1.8! Я попытался воссоздать ситуацию ниже:
CD / TMP rm -rf svn-repo svn-checkout свнадмин создать свн-репо svn checkout file:///tmp/svn-repo svn-checkout cd svn-checkout mkdir стволовые ветви эхо 'До свидания, мир!' > trunk/hello.txt SVN добавить ствол ветви svn commit -m 'Начальный импорт.' svn copy '^/trunk' '^/branch /rename' -m 'Создать ветку.' SVN-переключатель "^ / trunk". эхо "Привет, мир!" > hello.txt svn commit -m 'Обновление транка.' SVN переключатель '^ / Branches/ Rename'. SVN переименовать hello.txt hello.en.txt svn commit -m 'Переименовать в ветке.' SVN-переключатель "^ / trunk". svn merge --reintegrate '^/ Branches/ Rename'
Согласно книге, слияние должно завершиться чисто, но с неверными данными в переименованном файле, так как обновление на trunk
забыто Вместо этого я получаю конфликт дерева (это с Subversion 1.6.17, самой последней версией Debian на момент написания):
--- Объединение различий между URL-адресами репозитория в '.': Привет.en.txt C hello.txt Резюме конфликтов: Дерево конфликтов: 1
Никакого конфликта не должно быть - обновление должно быть объединено с новым именем файла. В то время как Subversion терпит неудачу, Mercurial обрабатывает это правильно:
rm -rf /tmp/hg-repo
hg init /tmp/hg-repo
cd /tmp/hg-repo
echo 'Goodbye, World!' > hello.txt
hg add hello.txt
hg commit -m 'Initial import.'
echo 'Hello, World!' > hello.txt
hg commit -m 'Update.'
hg update 0
hg rename hello.txt hello.en.txt
hg commit -m 'Rename.'
hg merge
Перед слиянием репозиторий выглядит так (из hg glog
):
@ changeset: 2: 6502899164cc | тег: совет | родитель: 0:d08bcebadd9e | пользователь: Martin Geisler | дата: чт 01 апр. 12:29:19 2010 +0200 | резюме: переименовать. | | o changeset: 1:9d06fa155634 |/ пользователь: Martin Geisler | дата: чт 01 апр. 12:29:18 2010 +0200 | резюме: обновление. | o changeset: 0:d08bcebadd9e пользователь: Martin Geisler дата: чт 01 апр. 12:29:18 2010 +0200 резюме: начальный импорт.
Результат слияния:
объединение hello.en.txt и hello.txt в hello.en.txt 0 файлов обновлено, 1 файлов объединено, 0 файлов удалено, 0 файлов не решено (слияние веток, не забудьте зафиксировать)
Другими словами: Mercurial взял изменение из ревизии 1 и объединил его с новым именем файла из ревизии 2 (hello.en.txt
). Разумеется, обработка этого случая необходима для поддержки рефакторинга, а рефакторинг - это именно то, что вы захотите сделать в ветке.
Не говоря об обычных преимуществах (коммиты в автономном режиме, процесс публикации, ...), вот пример "слияния", который мне нравится:
Основной сценарий, который я продолжаю видеть, - это ветвь, в которой... фактически разрабатываются две несвязанные задачи
(это началось с одной функции, но привело к разработке этой другой функции.
Или это началось с патча, но это привело к развитию другой функции).
Как вам слить только одну из двух функций на основной ветке?
Или как вы изолируете две функции в своих собственных ветках?
Вы можете попытаться сгенерировать какие-то патчи, проблема в том, что вы больше не уверены в функциональных зависимостях, которые могли бы существовать между:
- коммиты (или ревизии для SVN), используемые в ваших патчах
- другой совершает не часть патча
Git (и Mercurial, я полагаю) предлагает опцию rebase --onto для перебазирования (сброса корня ветви) части ветви:
- x - x - x (v2) - x - x - x (v2.1)
\
x - x - x (v2-only) - x - x - x (wss)
Вы можете распутать ситуацию, когда у вас есть патчи для v2, а также новая функция wss:
- x - x - x (v2) - x - x - x (v2.1)
|\
| x - x - x (v2-only)
\
x - x - x (wss)
, что позволяет вам:
- протестируйте каждую ветку изолированно, чтобы проверить, все ли скомпилировано / работает как задумано
- объединить только то, что вы хотите, чтобы основной.
Еще одна особенность, которая мне нравится (которая влияет на слияния) - это возможность сдавливать коммиты (в ветке, еще не перенесенной в другое репо), чтобы представить:
- более чистая история
- коммиты, которые являются более связными (вместо commit1 для function1, commit2 для function2, commit3 снова для function1...)
Это обеспечивает слияния, которые намного проще, с меньшим количеством конфликтов.
Мы недавно мигрировали из SVN в GIT и столкнулись с такой же неопределенностью. Было много неофициальных доказательств того, что GIT был лучше, но трудно было найти какие-либо примеры.
Я могу сказать вам, что GIT намного лучше при слиянии, чем SVN. Это явно анекдотично, но есть таблица для подражания.
Вот некоторые из вещей, которые мы нашли:
- SVN обычно создавал множество конфликтов в деревьях в ситуациях, когда казалось, что не должно. Мы никогда не доходили до сути, но в GIT этого не происходит.
- Хотя лучше, GIT значительно сложнее. Потратьте некоторое время на тренировки.
- Мы привыкли к черепахе SVN, которая нам понравилась. Черепаха GIT не так хороша, и это может отпугнуть вас. Однако теперь я использую командную строку GIT, которую я предпочитаю Tortoise SVN или любой из GIT GUI.
Когда мы оценивали GIT, мы проводили следующие тесты. Они показывают GIT как победителя, когда дело доходит до слияния, но не настолько. На практике разница намного больше, но я думаю, нам не удалось воспроизвести ситуации, которые SVN плохо обрабатывает.
Другие охватили более теоретические аспекты этого. Может быть, я могу дать более практическую перспективу.
В настоящее время я работаю в компании, которая использует SVN в модели разработки "ветки". То есть:
- Никакая работа не может быть сделана на стволе
- Каждый разработчик может создать свои собственные ветви
- Филиалы должны длиться в течение всего времени выполнения задачи
- У каждой задачи должна быть своя ветка
- Слияния обратно в транк необходимо авторизовать (обычно через bugzilla)
- В случаях, когда необходимы высокие уровни контроля, привратник может выполнять слияния
В общем, работает. SVN можно использовать для такого потока, но он не идеален. Есть некоторые аспекты SVN, которые мешают и формируют поведение человека. Это дает некоторые негативные аспекты.
- У нас было довольно много проблем с людьми, разветвляющимися от точек ниже, чем
^/trunk
, Это засоряет записи информации о слиянии по всему дереву и в конечном итоге нарушает отслеживание слияния. Ложные конфликты начинают появляться, и царит путаница. - Собрать изменения из ствола в ветку относительно просто.
svn merge
делает то, что вы хотите. Для объединения ваших изменений требуется (нам сказали)--reintegrate
по команде слияния. Я никогда по-настоящему не понимал этот переключатель, но это означает, что ветвь не может быть снова объединена с транком. Это означает, что это мертвая ветка, и вы должны создать новую, чтобы продолжить работу. (Смотрите примечание) - Весь процесс выполнения операций на сервере через URL-адреса при создании и удалении веток действительно сбивает с толку и пугает людей. Поэтому они избегают этого.
- Переключение между ветвями легко ошибиться, оставив часть дерева, глядя на ветвь А, а другую часть - на ветвь B. Поэтому люди предпочитают выполнять всю свою работу в одной ветке.
Обычно случается, что инженер создает филиал в первый день. Он начинает свою работу и забывает об этом. Через некоторое время приходит босс и спрашивает, может ли он выпустить свою работу в багажник. Инженер боялся этого дня, потому что реинтеграция означает:
- Слияние его долгоживущей ветки обратно в транк и решение всех конфликтов и выпуск несвязанного кода, который должен был быть в отдельной ветке, но не был.
- Удаляя свою ветку
- Создание новой ветки
- Переключение его рабочей копии на новую ветку
... и поскольку инженер делает это как можно меньше, он не может вспомнить "магическое заклинание", чтобы сделать каждый шаг. Происходят неправильные переходы и URL-адреса, и они внезапно оказываются в беспорядке и получают "эксперта".
В конце концов все успокаивается, и люди учатся исправлять недостатки, но каждый новый стартер сталкивается с одними и теми же проблемами. Возможная реальность (в отличие от того, что я изложил в начале):
- На стволе нет работы
- У каждого разработчика есть одна крупная ветка
- Ветви длятся до тех пор, пока работа не будет освобождена
- У исправленных ошибок, как правило, есть собственная ветка
- Слияния обратно к стволу выполняются после авторизации
...но...
- Иногда работа превращается в магистраль, когда не должна, потому что она находится в той же ветке, что и другая.
- Люди избегают всех слияний (даже легких вещей), поэтому люди часто работают в своих маленьких пузырьках
- Большие слияния, как правило, происходят и вызывают ограниченное количество хаоса.
К счастью, команда достаточно мала, чтобы справиться, но она не будет масштабироваться. Дело в том, что с CVCS все это не является проблемой, но более того, поскольку слияния не так важны, как в DVCS, они не так удобны. Это "трение слияния" вызывает поведение, которое означает, что модель "Feature Branch" начинает разрушаться. Хорошие слияния должны быть свойством всех VCS, а не только DVCS.
В соответствии с этим теперь --record-only
переключатель, который может быть использован для решения --reintegrate
проблема, и, очевидно, v1.8 выбирает, когда делать реинтеграцию автоматически, и это не приводит к тому, что ветвь будет мертвой
До Subversion 1.5 (если я не ошибаюсь) у Subversion был существенный недостаток в том, что он не запомнил историю слияний.
Давайте посмотрим на случай, изложенный VonC:
- x - x - x (v2) - x - x - x (v2.1)
|\
| x - A - x (v2-only)
\
x - B - x (wss)
Обратите внимание на ревизии A и B. Скажем, вы слили изменения из ревизии A в ветке "wss" в ветку "v2-only" в ревизии B (по любой причине), но продолжили использовать обе ветки. Если вы попытаетесь снова объединить две ветви, используя mercurial, он объединит изменения только после ревизий A и B. В Subversion вам придется объединить все, как если бы вы не делали слияние раньше.
Это пример из моего собственного опыта, когда слияние с B на A заняло несколько часов из-за объема кода: это было бы настоящей болью для повторения, что было бы в случае с Subversion pre-1.5.
Еще одно, вероятно, более важное отличие в поведении слияний от Hginit: перевоспитание Subversion:
Представьте, что вы и я работаем над некоторым кодом, и мы разветвляем этот код, и каждый из нас уходит в свои отдельные рабочие пространства и вносит множество изменений в этот код по отдельности, поэтому они сильно расходятся.
Когда нам нужно объединить, Subversion пытается просмотреть обе ревизии - мой измененный код и ваш измененный код - и пытается угадать, как их объединить в одну большую нечестивую путаницу. Обычно это дает сбой, создавая страницы и страницы "конфликтов слияния", которые на самом деле не являются конфликтами, просто места, где Subversion не удалось выяснить, что мы сделали.
Напротив, пока мы работали отдельно в Mercurial, Mercurial был занят сохранением серии изменений. И поэтому, когда мы хотим объединить наш код, Mercurial на самом деле имеет гораздо больше информации: он знает, что каждый из нас изменил и может применить эти изменения, а не просто смотрит на конечный продукт и пытается угадать, как его поместить. все вместе.
Короче говоря, анализ Mercurial различий (был?) Превосходит способ Subversion.