пятница, 3 апреля 2009 г.

Работа с ветками: push, pull, merge (Часть 3)

Рассмотрим еще один пример работы с ветками: совместная работа над проектом в небольшом коллективе. Для определенности будем рассматривать коллектив из 2х человек, поскольку приводимый сценарий легко масштабируется и на большее количество участников.

Сценарий №2

Над созданием программы под кодовым названием MegaApp работают Иван и Сергей. У них есть общее хранилище для веток проекта на центральном сервере. Там находится главная ветка проекта под названием trunk. Каждый из участников проекта, Иван и Сергей, имеет локальную копию главной ветки на своем компьютере и периодически синхронизирует ее с оригиналом на сервере.

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

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

Главная ветка проекта

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

Поскольку в распределенных системах все ветки равноправны, то участники проекта должны договориться какую ветку использовать в качестве главной. Имя такой ветки может быть произвольным, но наиболее часто используются такие имена: trunk, main, dev.
Имя trunk пришло из svn. По английски "trunk" означает "ствол" (ствол дерева), как противоположность просто веткам (branch), которые ответвляются от ствола.

Центральное хранилище

Для хранения эталонной главной ветки, с которой все участники проекта будут периодически синхронизироваться, нужно какое-либо центральное хранилище, или центральный сервер. Базар поддерживает различные типы серверов для хранения веток: SFTP, FTP, специализированный bzr-сервер, можно даже задействовать какой-то общий каталог на одной из машин (в сети Windows) или на SAMBA-сервере. Выбор того или иного варианта зависит от конкретной ситуации.

Далее в примерах будет использоваться локальный bzr-сервер, запущенный командой:
bzr serve --allow-writes --directory C:/work/bzr-day/server
где опция --directory указывает каталог, в котором располагаются ветки. Используйте любой другой удобный для вас каталог.
Будьте внимательны: bzr-сервер не осуществляет никакую авторизацию пользователей, и с опцией --allow-writes разрешает любому клиенту операции как чтения так и записи. bzr-сервер использует порт 4155 для работы.

Создание главной ветки на сервере

Иван использует команду init (или push), чтобы создать ветку trunk на сервере:

C:\work\bzr-day\ivan>bzr init bzr://localhost/trunk
Created a standalone branch (format: unnamed)

Создание локальной копии главной ветки

Для работы над проектом Ивану необходима локальная копия главной ветки. Для получения главной ветки с сервера Иван использует команду branch:

C:\work\bzr-day\ivan>bzr branch bzr://localhost/trunk
Branched 0 revision(s).

Ветка совсем пустая (0 revisions), поэтому Иван решает сделать первую фиксацию прямо в ней и обновить ветку на сервере:

C:\work\bzr-day\ivan\trunk>bzr commit -m initial --unchanged
Committing to: C:/work/bzr-day/ivan/trunk/
Committed revision 1.

C:\work\bzr-day\ivan\trunk>bzr push bzr://localhost/trunk
Pushed up to revision 1.

Рекомендации по работе над проектом

Участники проекта могут изменять файлы и фиксировать новые ревизии прямо в своей копии ветки trunk, однако целесообразнее использовать отдельные ветки (так называемые feature branches или функциональные ветки). Для чего это нужно?

При работе над сложным проектом редко когда новую функцию удается реализовать быстро, гораздо чаще требуется длительный промежуток времени (больше чем 5-10 минут). Работа над сложными функциями может состоять из реализации нескольких последовательных этапов работы. Каждый такой логический этап можно (и нужно) фиксировать командой commit. При этом что-то может быть недоделано или программа вообще откажется работать. Фиксировать неработоспособный код прямо в trunk — это очень плохая идея. Ваш коллега по работе может взять новый неработоспособный код, не подозревая об этом, и получить в нагрузку кучу неприятной головной боли. Всегда старайтесь держать в trunk только работоспособный (комплирующийся, протестированный) код.

Еще одним плюсом функциональных веток является логическая группировка ревизий. Гораздо удобнее анализировать историю проекта, когда ревизии, относящиеся к реализации конкретной функции, идут в журнале вместе. Если же каждый разработчик фиксирует свои изменения прямо в trunk и часто делает push в главную ветку, то в результате история изменений становится запутанной, как куча спагетти. Хотите ли вы этого?

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

Иван работает над новой функцией

Иван начинает работу над новой функцией с того, что обновляет свою локальную копию главной ветки, и затем создает отдельную ветку для работы:

C:\work\bzr-day\ivan\trunk>bzr pull
Using saved parent location: bzr://localhost/trunk/
No revisions to pull.

C:\work\bzr-day\ivan\trunk>cd ..

C:\work\bzr-day\ivan>bzr branch trunk feature
Branched 1 revision(s).

C:\work\bzr-day\ivan>cd feature

Иван пишет новый код, фиксирует каждый логически законченный этап, тестирует его...

C:\work\bzr-day\ivan\feature>bzr st
...
C:\work\bzr-day\ivan\feature>bzr diff
...
C:\work\bzr-day\ivan\feature>bzr commit ...
...
C:\work\bzr-day\ivan\feature>bzr log
...

Наконец, работа над новой функцией закончена и Иван объединяет главную ветку со своей работой:

C:\work\bzr-day\ivan\feature>cd ../trunk
C:\work\bzr-day\ivan\trunk>

C:\work\bzr-day\ivan\trunk>bzr merge ../feature
+N  foo.c
All changes applied successfully.

C:\work\bzr-day\ivan\trunk>bzr status
added:
  foo.c
pending merge tips: (use -v to see all merge revisions)
  Базарный день 2009-03-31 Реализована и протестирована новая функция foo

C:\work\bzr-day\ivan\trunk>bzr status -v
added:
  foo.c
pending merges:
  Базарный день 2009-03-31 Реализована и протестирована новая функция foo
    Базарный день 2009-03-31 Исправлена ошибка в функции foo
    Базарный день 2009-03-31 Реализован базовый алгоритм функции foo

C:\work\bzr-day\ivan\trunk>bzr commit -m "Реализована функция foo"
Committing to: C:/work/bzr-day/ivan/trunk/
added foo.c
Committed revision 2.

После чего можно отправлять новые ревизии на сервер командой push:

C:\work\bzr-day\ivan\trunk>bzr push bzr://localhost/trunk
Pushed up to revision 2.

Подобным образом работают все участники проекта.

Сергей работает над исправлением найденного дефекта

Работа над исправлением дефекта для Сергея мало отличается от написания новой функции программы. Сергей точно также использует отдельную ветку для работы и в конце объединяет главную ветку со своими изменениями:
  • обновить локальную копию главной ветки (bzr pull)
  • создать отдельную ветку для работы (bzr branch trunk bugfix)
  • проделать работу в отдельной ветке (status, diff, commit, log)
  • объединить локальную копию главной ветки со своими изменениями (bzr merge ../bugfix)
  • передать новые ревизии в главную ветку на центральном сервере (bzr push)

Регулярная синхронизация с главной веткой

При длительной уединенной работе в отдельной ветке следует помнить о том, что ваши коллеги по проекту не спят и тоже пишут новый код. Поэтому чем дольше вы работаете над новыми функциями или багфиксами, тем выше вероятность, что кто-то из коллег сделает изменения, которые будут влиять на вашу работу. Это может быть и простое исправление опечатки в коде или комментариях, массивный рефакторинг, либо просто частичное изменение API используемых вами модулей. В первых двух случаях у вас возникнут конфликты при объединении вашей законченной работы и trunk. В случае изменения API вы получите неработоспособную программу. Поэтому необходима регулярно обновлять свою копию ветки trunk и синхронизировать свою работу с ней.
Рассмотрим следующий пример. Иван работает над новой сложной функцией программы не первый день. В это время Сергей исправляет найденный дефект в коде одного из модулей, и свои исправления публикует в ветке trunk. Исправления Сергея затрагивают код, над которым работает Иван. Иван время от времени обновляет копию главной ветки и просматривает новые ревизии. Когда он замечает, что появились изменения, влияющие на его работу, то Иван объединяет новые ревизии из trunk со своей веткой, исправляет возможные несоответствия в своем коде и продолжает работу в своей ветке.

Вот как этот пример выглядит в командах bzr:

Иван работает в своей ветке bigfeature над новой сложной функцией:

bzr branch trunk bigfeature
cd bigfeature
hack-hack-hack
bzr diff
bzr commit

Иван периодически обновляет свою копию trunk, просматривает новые ревизии и обнаруживает появление багфикса от Сергея:

cd ../trunk
bzr pull
bzr log

Для того, чтобы увидеть только новые ревизии в главной ветке, которые отсутствуют в ветке bigfeature Иван может воспользоваться командой missing:

cd ../bigfeature
bzr missing --theirs-only ../trunk

Флаг --theirs-only указывает команде missing, что пользователя интересует только информация о новых ревизиях в ветке trunk.

Иван решает, что ему нужно объединить новые правки из trunk с его собственной работой и делает merge:

cd ../bigfeature
bzr merge ../trunk
bzr diff
bzr commit -m "merge trunk"

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

Резюме

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

11 комментариев:

  1. А можно общий вопрос? А как понял -- кардинальное отличие, например, git от bzr в том, что git хранит все ветки физически в одном каталоге и можно просто "переключать" рабочие файлы с одной ветки на другую схожим способом, как идет переключение между ревизиями. Это позволяет нескольким веткам использовать совместное хранилище, тем самым экономя место на диске.

    В bzr же создание ветки всегда происходит в физически другом каталоге. То есть создание ветки в bzr - это создание нового каталога, хранящего всю историю. Правильно ли я понимаю, что каждая такая ветка - это полное хранилище всей истории? Если это так, то если история моего проекта занимает два гига, то при создании каждой ветки происходит клонирование этих двух гигов? так? или тут есть тонкость...

    ОтветитьУдалить
  2. И еще вопрос: какая тулза для разрешения конфликтов в графическом режиме? ну как p4merge в Perforce? Просто когда конфликтов десятки, то такие средства сильно ускоряют процесс.

    ОтветитьУдалить
  3. > какая тулза для разрешения конфликтов в графическом режиме? ну как p4merge в Perforce?

    Любая, какая вам нравится. Утилита для разрешения конфликтов автоматически не вызывается. Можно задать свою утилиту объединения в команде merge, но тогда каждый файл будет обрабатываться ею.

    При помощи плагина extmerge можно облегчить запуск одной из популярных графических утилит. И затем запускать для каждого файла из командной строки.

    Либо использовать плагин QBzr, у него есть GUI-диалог для просмотра списка конфликтов. Из этого диалога можно запускать выбранную утилиту для разрешения конфликтов в графическом режиме.

    ОтветитьУдалить
  4. По поводу веток и хранилища истории:

    в bzr можно использовать общий репозиторий (shared repository) разделяемый между несколькими ветками. В этом случае каждая ветка хранит только указатель на последнюю ревизию истории + некоторую служебную информацию. А вот сами ревизии (те самые 2ГБ) будут храниться в общем репозитории. И при создании новой ветки внутри общего репозитория имеющаяся история дублироваться не будет.

    Дополнительно можно создать только одну рабочую копию и хранить ветки в общем репозитории без рабочих файлов. А рабочую копию переключать между ветками.

    Это примерный аналог модели гита: один репозиторий + много веток. Я сам часто использую такой вариант.

    Впрочем главные разработчики осознали преимущества модели гита и возможно в будущем появится возможность использовать гито-подобную организацию репозиториев (с виртуальными ветками).

    ОтветитьУдалить
  5. Так, по первому вопросу ответ найден: init-repository. В целом - очень удобный подход. Можно и копировать всю историю каждый раз (например, для небольших проектов), а можно и через shared repository. Очень хорошо.

    ОтветитьУдалить
  6. Кстати, подход с ветками в каталогах не так уж и плох. Я, например, очень люблю ветки windiff'ов сравнить. В git'е их надо checkout'ить в разные каталоги (другого способа я не нашел), а тут просто сравнить каталоги, так как ветки и так там живут. А в bzr получается, что если сделать shared repository, то и выходит схема git'а.

    ОтветитьУдалить
  7. да init-repo создает новый общий репозиторий.

    команда bzr init-repo --no-trees создат общий репозиторий, в котором ветки по умолчанию будут создаваться без рабочих файлов.

    ОтветитьУдалить
  8. Отлично, прикрутил qbzr и extmerge + kdiff3. Очень неплохо. Осталось самое главное - погонять все это на больших объемах. Сейчас объем нашего depot в Perforce - ~70гигов. Конечно, это все проекты с сумме. Отдельный-то релиз потянет гига на 3-4. Посмотрим.

    ОтветитьУдалить
  9. Perforce имеет славу очень быстрой системы, которая нормально справляется с большими объемами репозитариев и количеством файлов.

    bzr временами имеет проблемы с этим. Надеюсь, впрочем услышать ЛЮБОЙ ваш отзыв по итогам испробования.

    В качестве небольшой оптимизации для работы с большими репозиториями лучше использовать формат репозитария 1.9. Для этого при инициализации общего репозитария и/или ветки используйте опцию --1.9, например:

    bzr init-repo --1.9
    bzr init --1.9

    Для ваших объемов это должно помочь.

    ОтветитьУдалить
  10. Скажите, а зачем Ивану и Сергею создавать свои собственные ветки-копии trunk? Почему просто не сделать checkout?
    Т.е. они постоянно синхронят свои trunk и trunk сервера через push\poll - по идее это как раз случай когда chekout рулит. Например он не позволит сделать commit, когда локальный trunk стал неактуальным по сравнению с trunk на сервере...
    Спасибо

    ОтветитьУдалить
  11. Это основная идея распределенных систем. Ты работаешь со своей веткой, в своем стиле. При правильной организации работ объединение не вызывает никаких проблем.

    В этом случае локальная ветка после commit может быть в каком угодно состоянии, главное, что бы она была в правильном состоянии при push. И тебя не беспокоит, что ты, вдруг, не можешь просто сделать commit в случае изменений в основной ветке. ;-) Обычно в основную ветку имеет смысл заливать уже какой-то законченный функционал, или исправление ошибки.

    ОтветитьУдалить