Сравнение DVCS - несколько задач — различия между версиями
(→Mercurial) |
(→Git) |
||
Строка 42: | Строка 42: | ||
=== Git === | === Git === | ||
− | '''Git''': | + | '''Git''': отлично (5-)! <tt>[http://git-svn.yhbt.net/ git-svn]</tt> встроен в git и умеет всё, что нужно: клонирование, синхронизация (<tt>fetch</tt>, <tt>pull</tt> или <tt>merge</tt>), фиксация изменений в Subversion-репозиториях (<tt>dcommit</tt>) и <tt>rebase</tt> работают с сохранением веток и меток, причём стандартная схема их именования <tt>trunk/branches/tags</tt> не является обязательной. Ветки SVN импортируются как [http://www.gitready.com/beginner/2009/03/09/remote-tracking-branches.html Remote Tracking Branches]. Можно начинать историю ветки в git’е не с сотворения миров, а с любого момента. Также <tt>git-svn</tt> поддерживает подключаемые внешние репозитории Subversion (т. н. [http://svnbook.red-bean.com/en/1.0/ch07s03.html Externals]). Граф ветвлений сохраняется, хоть и не так круто, как в Mercurial’е. В общем, можно сказать, что функционал полон. Как всегда с Git’ом, есть различные нетривиальности, что-то бывает нужно настроить (''гитара настроена? нас двое, на! играй, на!''), синтаксис не совмещён с обычными командами Git — то есть например для синхронизации копии с Subversion нужно сказать не <tt>git pull</tt>, а <tt>git svn fetch</tt>, посему всё-таки 5-, а не твёрдое 5. |
+ | |||
+ | Кстати, проходит (пока ещё проходит, продолжаясь уже пару дней) очень мощный тест — полное клонирование [http://svn.wikimedia.org/ SVN-репозитория Wikimedia]. Пару раз прерывалось из-за каких-то сетевых катаклизмов, но по <tt>git svn fetch</tt> успешно возобновлялось. | ||
=== Bazaar === | === Bazaar === |
Версия 15:06, 26 ноября 2009
Данная статья является очередным сравнением популярных DVCS — Mercurial, Git и Bazaar, с точки зрения нескольких нетривиальных задач.
Содержание
Работа с SVN (миграция и синхронизация)
Почему-то, говоря о недостатках DVCS, никогда не говорят о следующем не очень удобном при переходе с централизованных систем поведении — DVCS отслеживают версию цельного репозитория, а не отдельных файлов или директорий. Кроме проблем, которые это создаёт при желании извлечь часть репозитория для работы, а не обязательно весь, это ведёт к тому, что если две ветки «расходятся», становится обязательным слияние, даже если конфликтов в изменениях не было и даже если вообще менялись разные файлы. А централизованным хоть бы хны — svn up и живи себе дальше.
Что ещё любопытно — с Subversion-репозиториями DVCS работают, как правило, быстрее самого Subversion, и быстрее всех работает Bazaar. То есть, в принципе, можно вообще жить с Subversion-сервером и DVCS-клиентом (если, конечно мириться с постоянными слияниями и rebase’ами).
Mercurial
Mercurial: отлично (5-)! Есть несколько расширений — hgsubversion, hgsvn, convert, позволяющих работать с Subversion тем или иным образом, и не совместимых друг с другом. Самое вменяемое из них — hgsubversion, хотя и заявлено, что оно ещё сырое, и, к сожалению, не распространяется вместе с Mercurial’ом. Имеет фактически весь необходимый функционал — можно делать и push, и pull в/из Subversion, можно клонировать SVN-репозиторий с сохранением веток и меток (правда, обязательно стандартное их расположение в корневых поддиректориях /trunk, /branches, /tags), эти два метода совместимы, rebase также работает, а граф ветвлений сохраняется. Очень крут тот факт, что ветка, которая создавалась неполным копированием trunk'а, то есть, копированием некоторых его поддиректорий, успешно подцепилась в нужное место графа ветвлений. Ни Bazaar, ни Git этого не смогли.
Вместо 5 всё-таки 5-, так как был обнаружен очень неприятный баг: push из mercurial-копии репозитория без веток и меток (то есть вида просто одной директории) в Subversion не работал вообще — обсуждение в группе hgsubversion. К счастью, меня пропёрло, я потратил час времени и баг этот пофиксил собственноручно — фикс очень простой, на две строчки:
--- hgsubversion/pushmod.py 2009-11-18 21:04:05.697192616 +0300 +++ hgsubversion/pushmod.py 2009-11-26 04:14:39.000000000 +0300 @@ -158,7 +158,7 @@ def commit(ui, repo, rev_ctx, meta, base file_data[file] = base_data, new_data, action def svnpath(p): - return ('%s/%s' % (branch_path, p)).rstrip('/') + return ('%s/%s' % (branch_path, p)).strip('/') changeddirs = [] for d, v1, v2 in extchanges: @@ -181,7 +181,7 @@ def commit(ui, repo, rev_ctx, meta, base new_target_files = [svnpath(f) for f in file_data] for tf, ntf in zip(file_data, new_target_files): - if tf in file_data: + if tf in file_data and tf != ntf: file_data[ntf] = file_data[tf] if tf in props: props[ntf] = props[tf]
Насколько я понимаю, он уже очень скоро попадёт в репозиторий, поэтому итоговая оценка почти не снижается. Тем не менее, такие грабли можно расценить как показатель некой «сырости» продукта — об этом всё-таки надо помнить.
Остальные два экстенжна «нинужны»: hgsvn — нечто более старое, работает сбоку от общего механизма, тоже позволяет делать push и pull, но не клонирует весь репозиторий, а только извлекает (checkout’ит) последнюю версию, чтобы далее можно было использовать Subversion и Mercurial вместе. Ну и конечно, оно не совместимо с hgsubversion. convert же предназначен для конвертации истории проекта из нескольких различных систем контроля версий в Mercurial, ни черта не совместим ни с hgsvn, ни с hgsubversion и не сохраняет граф ветвлений. Зато, правда, поддерживает возможность использования других имён поддиректорий trunk/branches/tags.
Git
Git: отлично (5-)! git-svn встроен в git и умеет всё, что нужно: клонирование, синхронизация (fetch, pull или merge), фиксация изменений в Subversion-репозиториях (dcommit) и rebase работают с сохранением веток и меток, причём стандартная схема их именования trunk/branches/tags не является обязательной. Ветки SVN импортируются как Remote Tracking Branches. Можно начинать историю ветки в git’е не с сотворения миров, а с любого момента. Также git-svn поддерживает подключаемые внешние репозитории Subversion (т. н. Externals). Граф ветвлений сохраняется, хоть и не так круто, как в Mercurial’е. В общем, можно сказать, что функционал полон. Как всегда с Git’ом, есть различные нетривиальности, что-то бывает нужно настроить (гитара настроена? нас двое, на! играй, на!), синтаксис не совмещён с обычными командами Git — то есть например для синхронизации копии с Subversion нужно сказать не git pull, а git svn fetch, посему всё-таки 5-, а не твёрдое 5.
Кстати, проходит (пока ещё проходит, продолжаясь уже пару дней) очень мощный тест — полное клонирование SVN-репозитория Wikimedia. Пару раз прерывалось из-за каких-то сетевых катаклизмов, но по git svn fetch успешно возобновлялось.
Bazaar
Bazaar: хорошо. Можно импортировать репозиторий командами svn-import или branch, можно делать push и pull в/из Subversion. При импорте можно сохранить все ветки и метки в одном хранилище (если использовать Shared Repository), для этого также требуются стандартные названия trunk/branches/tags. Граф ветвлений Subversion сохраняется, но также не отражает копирования подпапок. Также существует несколько других расширений для импорта Subversion в Bazaar, но они хуже.
И всё было бы замечательно, но есть одна очень неприятная проблема. Причём возникающая в достаточно простом случае. То есть пусть бы она и появлялась, но очень редко — это можно терпеть. Но тут ситуация довольно частая.
Так вот. Всё начинается с банального — мы сделали изменение в Bazaar-клонированном SVN-репозитории, а кто-то тем временем сделал изменение в самом SVN-репозитории… Причём не важно, в каких файлах: даже если конфликтов в наших изменениях нет, проблема возникает всё равно (помним о том, что DVCS управляют «цельным» репозиторием). Что нам теперь остаётся делать? Ну конечно, мержиться. И merge успешно проходит. Но в случае Bazaar помните — после этого merge нужно сразу делать rebase и push в Subversion! Иначе, если до этого сделать ещё хотя бы один commit в Bazaar, push не удастся, а rebase не поможет или вывалится с исключением.
Хотите подробнее? Рассмотрим следующую последовательность действий:
- Клонируем svn в ветку bzr1.
- Клонируем bzr1 в bzr2.
- Вносим изменения в bzr2, коммитим.
- Вносим другие изменения в svn, коммитим. Лучше несколько раз (создаём несколько ревизий).
- Делаем pull из svn в ветку bzr1 — bzr1 снова синхронизирован с svn.
-
Делаем push из bzr2 в bzr1 — облом: ветки «разошлись». Хорошо, делаем merge из bzr1 в bzr2. Коммитим (фиксируем). - (важно) Снова вносим изменения в bzr2, коммитим.
- Теперь хотим протащить изменения из bzr2 в svn. Сначала делаем push из bzr2 в bzr1. Теперь история bzr1 идентична истории bzr2.
- И это не так тривиально, как хотелось бы!
- Потому что теперь импортированные из svn в bzr1 ревизии заменяются одной merge-ревизией, а после неё в истории появляется ревизия с модификацией, импортированная из bzr2. Исходные svn-ревизии «подцепляются» к merge-ревизии. Чтобы увидеть их, нужно сказать не просто bzr log, а bzr log -n0.
-
Делаем push из bzr1 в svn — облом: в bzr1 есть ревизия, «воткнутая» между уже зафиксированными в репозитории svn-ревизиями.
Две bzr-ветки здесь необязательны — просто полезны для иллюстрации танцев ревизий.
И что теперь делать?
bzr: ERROR: Operation denied because it would change the mainline history. Set the append_revisions_only setting to False on branch "..." to allow the mainline to change.
Bazaar предлагает нам разрешить менять местами ревизии в SVN-репозитории. Но если разрешить — всё равно обламывается (ну кто ж тебе по WebDAV даст ревизии местами переставить, даа) и предлагает использовать rebase. А rebase — и это из-за пункта 7 — нам не поможет, а скажет «no revisions to rebase». А в некоторых комбинациях — вывалится со змеиным исключением.
Чтобы исправить эту ситуацию:
- Клонируем svn ещё раз в ветку bzrtmp.
- Делаем merge из bzr1 в bzrtmp, коммитим. Теперь в bzrtmp последней ревизией будет merge-ревизия, к которой «подцеплены» ревизии, которые мы так жаждем протащить-таки в SVN.
- Теперь мы можем сделать push из bzrtmp в svn, а потом из svn — pull во все остальные ветки, и они придут к согласованному виду…
Хотите автоматический сценарий для воспроизведения ошибки? Пожалуйста: bzr-rebase-dont-help.sh. В конце видны результаты, которые дадут команды rebase и pull. В частности, в результате последней команды rebase bzr скажет «ERROR: exceptions.AttributeError: 'NoneType' object has no attribute 'get_known_graph_ancestry'», уйдёт в Python Traceback и оттуда не вернётся.
#!/bin/sh svnadmin create test svn co file://`pwd`/test test-co cd test-co echo "helloworld" > a echo "diepeople" > b svn add a b svn ci -m "add a+b to svn" cd .. bzr branch `pwd`/test test-bzr cd test-co echo "tretiynah" > c svn add c svn ci -m "add c to svn" cd .. cd test-bzr echo "rozoviyslonik" > d bzr add d bzr ci -m "add d to bzr" bzr merge bzr ci -m "merge to bzr" echo "1nah" > a bzr ci -m "change a in bzr" bzr push `pwd`/../test # ФИГВАМ! Operation denied because it would change the mainline history. (логично) bzr rebase -r 5 # ФИГВАМ! No revisions to rebase. bzr rebase --onto=2.1.1 # ФИГВАМ! ERROR: exceptions.AttributeError: 'NoneType' object has no attribute 'get_known_graph_ancestry' а дальше идёт Python traceback bzr rebase -r 2 # Проходит, но изменения, внесённые в bzr, пропадают бесследно и проносить в svn становится нечего.
А в Mercurial в точно такой же ситуации rebase замечательно работает. Пример аналогичного скрипта: hg-rebase-better-than-bzr-ithelps.sh.
#!/bin/sh svnadmin create test svn co file://`pwd`/test test-co cd test-co echo "helloworld" > a echo "diepeople" > b svn add a b svn ci -m "add a+b to svn" cd .. hg clone file://`pwd`/test test-hg cd test-co echo "tretiynah" > c svn add c svn ci -m "add c to svn" cd .. cd test-hg echo "rozoviyslonik" > d hg add d hg ci -m "add d to hg" hg pull hg merge hg ci -m "merge to hg" echo "1nah" > a hg ci -m "change a in hg" #hg push file://`pwd`/../test # ФИГВАМ! Sorry, can't find svn parent of a merge revision. echo "5nah" > e hg add e hg ci -m "add e to hg" hg rebase -d 2 hg push
Управление патчами
Рассмотрим задачу управления набором патчей: есть некоторый проект, есть набор сторонних (например, ваших) патчей. Некоторые из них нужно накатывать в определённой последовательности, кроме того, и само ПО, и патчи не стоят на месте и могут меняться, и неплохо бы иметь также их историю. Можно, конечно, просто создать форк, но в этом случае ваша ветка, скорее всего, серьёзно разойдётся с оригинальной и «выделять» из неё отдельные фичи станет очень тяжело. Короче говоря, если кому-то хочется увидеть наглядный пример из разряда «нафига оно надо», выполните команду apt-get source mc, то есть, скачайте Debian-пакеты с исходными кодами для Midnight Commander — патчей там чуть меньше, чем 50.
quilt, MQ, bzr-loom, StGIT
Первое, что приходит на ум — это, конечно, аналоги quilt'а, работающие поверх DVCS: Mercurial Queues (MQ), Bazaar Loom, StGIT. Все они очень похожи и друг на друга, и на сам quilt. MQ и StGIT достаточно развиты и стабильны. bzr-loom же пока находится на стадии развития.
quilt — вещь банальная, позволяющая автоматизировать тупое накатывание последовательности большого числа патчей и правку патча, который находится где-то в середине: можно откатить N верхних патчей, внести изменения в файлы и сказать refresh, и верхний на данный момент патч (то есть патч откуда-то из середины) обновится, дабы соответствовать внесённым изменениям. quilt создан на основе скриптов человека, который не использует системы контроля версий — Эндрю Мортона (Andrew Morton) — второго по значимости участника разработки ядра Linux после Линуса Торвальдса.
Из реализаций quilt поверх DVCS первым появился именно MQ и долгое время, судя по всему, был «изюминкой» Mercurial’а, хотя на самом деле совсем не идеален — идея добавления и удаления истории из/в репозиторий всё-таки странновата, а патчи не хранятся в том же репозитории, что и код — то есть, при клонировании исчезают. Этого недостатка лишены и bzr-loom, и StGIT.
Подход «по ветке на патч»
Крут ли quilt, нет ли — каждый решает сам для себя. Конечно, поддержке Debian-патчей на какой-нибудь Midnight Commander quilt очень… помогает. Тем не менее, с моей точки зрения, некорректно закладываться на жёстко последовательное применение всех патчей. На самом деле, такие патчи логично организовывать в виде графа (графа зависимостей). Сразу будет видно, что большинство патчей независимы друг от друга, а реально существующие зависимости окажутся на виду.
Теперь вспомним, что мы рассматриваем DVCS и что они умеют очень хорошо управлять ветками, а ревизии как раз организуются в граф — и поймём, что задачу управления патчами удобнее всего решать, заводя по отдельной ветке на каждый патч. Причём для этого есть даже расширения Mercurial pbranch (от patch branches) и Git TopGit (от topic branches). Для Bazaar’а подобного расширения, увы, нет.
Mercurial pbranch
Mercurial'овский pbranch: хорошо. Умеет весьма немногое. Самые полезные команды — это pdiff, pgraph и pmerge. pdiff экспортирует текущий патч без его родителей в стандартном формате. pgraph показывает граф зависимостей патчей, из которого можно, по сути, понять, в какой последовательности их накатывать, если они сами уже экспортированы. Ну и просто вообще граф патчей — это удобно. pmerge объединяет изменения, произошедшие в зависимостях патча, в патч.
Другие команды расширения:
- pbackout
- отменить текущий патч.
- pmessage
- посмотреть описание патча.
- peditmessage
- изменить описание патча.
- pemail
- отправить патч по почте, имхо — бесполезная вещь.
- pnew
- создать новый патч из ещё не закоммиченных изменений в текущей ветке.
- pstatus
- посмотреть статус патча (какие изменения требуются).
- reapply
- применить некую уже закоммиченную ревизию в ветку патча.
Чего не хватает (а хочется):
- легко создавать патчи, зависящие от нескольких других.
- легко поддерживать ветку, содержащую объединение всех патчей — для развёртывания.
- удалять зависимости патчей.
Хотя и то, и другое, в принципе, сделать можно и так. Плюс же расширения заключается в хорошей online-документации — у TopGit такой нет…
TopGit
TopGit: тоже весьма хорошо, даже чуть получше (версия на момент тестирования была 0.8). А в будущем станет ещё лучше — по ходу просмотра справки по некоторым командам выводится по одному-два TODO — значит, они, наверное, появятся.
Забавно, кстати, что TopGit явно написан в общем духе git-а — смесь скриптов на bash, perl и программ на C. «Головной» скрипт tg (TopGit) написан на шелле, и команды tg --help, tg -h, tg help и т. п. сначала вводят в недоумение — «no git repository here». Нет, не подумайте, справку TopGit печатать умеет, но для этого сначала надо зайти в git-репозиторий :) типа Чего ты спрашиваешь? У тебя интерес реальный? Ты покупать будешь или нет?..
TopGit, как уже сказано, умеет побольше pbranch, в частности, умеет 1-ую и 2-ую вышеупомянутые «хотелки» — команда tg create может создать новую ветку-патч на основе множества зависимостей, автоматически объединяя их и предлагая разрешать возникающие конфликты, а с помощью tg update можно обновлять изменения в зависимостях в ветки патчей, причём рекурсивно. На самом деле и то, и другое можно сделать и в pbranch, и даже вообще без него, но придётся набрать чуть больше команд.
Команды расширения:
- create
- создать новую ветку-патч на основе одной или нескольких веток.
- delete
- удалить ветку-патч.
- depend
- редактировать зависимости.
- export
- экспортировать историю текущей ветки в виде quilt-серии или новой, «подчищенной», git-ветки.
- import
- импортировать коммиты в виде патчей.
- info
- аналог hg pstatus, выводит состояние ветки — сколько, чего и откуда надо обновить и т. п.
- отправка патчей по почте.
- patch
- аналоги hg diff, выводит текущий патч в виде diff + комментарий вверху.
- push
- отправляет изменения из tg-шной ветки.
- remote
- перевести внешние (remote-tracking) ветки под управление tg.
- summary
- выводит список патчей со статусами и/или граф зависимостей через Graphviz. В ASCII-виде этот граф, к сожалению, не рисуется.
- update
- см.выше.
Чего не хватает (а хочется):
- удалять зависимости патчей.
То есть — реально удалять зависимости, а не просто отменять изменения. Но это в TODO перечислено и, вероятно, будет реализовано.
Схема управления рабочими копиями
Что нам предлагают централизованные системы контроля версий? Некий «хардкод»: репозиторий ровно один, рабочих копий много. Что нам предлагают DVCS? Теоретически, полный беспредел, то есть свободу.
На практике — не совсем полную.
Mercurial говорит нам: на ровно 1 рабочую копию ровно 1 репозиторий, и иначе быть не может. Это и удобно — сказал hg up ветка_такая_то, пара файлов поменялась, и опа — ты уже в другой ветке. Это и неудобно — чтобы положить на диск одновременно две разных ветки, нужно обязательно клонировать репозиторий (поддержки checkout нет).
С Git'ом ситуация почти такая же, как и с Mercurial’ом. 1 рабочая копия, 1 репозиторий. Хотя при клонировании данные репозитория можно и не копировать, задавая опцию --shared, но это скорее похоже на Bazaar’овские Stacked Branches, чем на Lightweight Checkout. Идея checkout’ов, или лёгких рабочих копий (или «идея .gitlink») высказана для GSoC-2007, однако пока так и не реализована.
Управление ветками в Bazaar вначале было гораздо хуже: на 1 ветку ровно 1 рабочая копия и ровно 1 репозиторий. Однако потом появилось уникальное: checkout'ы и Shared Repository, имеющий опцию --no-trees. Таким образом, стало возможно иметь сколько угодно репозиториев и сколько угодно рабочих копий. В Git и Mercurial этого нет. Переключать легковесную рабочую копию с ветки на ветку Bazaar тоже умеет — командой switch, для переключения ветки её надо превратить в легковесную рабочую копию командой bind.
Тут надо сделать небольшое отступление: Bazaar имеет несколько идеологических моментов. Первый — это идея, гласящая, что директории — не контейнеры веток, а сами ветки. В одной директории, содержащей рабочую копию, не может содержаться нескольких веток. Существуют ещё разделяемые репозитории, но они сами по себе не содержат рабочей копии, а содержат директории, которые могут содержать рабочие копии.
Идея, с моей точки зрения, далеко не лучшая. Например, из-за неё Bazaar не может, как Git и Mercurial, переключиться на произвольную ревизию и по коммиту автоматически ответвиться в сторону. В Bazaar можно только сделать revert («откат») к определённой версии. Рабочие файлы при этом изменятся, а «состояние» рабочей копии — нет. Считается, что рабочая копия всегда содержит последнюю сохранённую ревизию.
Вторая идея — это идея mainline, то есть, идея поддержки «центральной» ветки разработки на уровне системы контроля версий. С хвалебной точки зрения про mainline можно почитать в блоге «Базарный день» (части 1, 2, и будут ещё). Я же выступлю с критической точки зрения — никакая это не гениальная практика, а просто ещё один хардкод, который можно выразить простыми словами: после синхронизации рабочих копий HEAD-ревизия (ревизия, не имеющая дочерних — в терминах Mercurial), всегда ровно одна. Все другие HEAD’ы физически находятся в других директориях (= ветках), ещё не синхронизированных с данной. То есть, Bazaar заставляет пользователя объединять историю при каждой синхронизации с другой веткой.
Из идеи mainline сразу следует определённое неудобство: при синхронизации меняется история веток (даже твоей, любимой, неприкосновенной) — ревизии, внесённые ранее локально, могут быть перемещены в «под-узлы» ревизии-объединения, которую в свою ветку внесла удалённая сторона. История меняется так, чтобы в конечном счёте история всех веток выглядела одинаково. То есть, «история истории» теряется, а ещё из перестановок ревизий в истории сразу следуют проблемы работы с Subversion.
Короче говоря, да, это просто очередной «хардкод». То ли — рассчитанный на то, что если дать людям свободу, они не будут в состоянии достаточно часто объединяться с основной веткой, то ли — просто унаследованный от Arch’а (GNU Arch — прародитель Bazaar’а).
И если уж зашла речь о недостатках Bazaar’а, ещё следует отметить, что он не умеет показывать консольные ASCII-псевдографические графы ревизий. Git и Mercurial умеют — и это очень удобно. А разработчики Bazaar считают их отсутствие фичей — ревизии, типа, сортируются, зачем графы? (и правда, зачем? стройте графы сами, в голове! как не умеете?!!)