Типовые примеры

Ветвлением и слиянием можно пользоваться по-разному. В этом разделе описываются наиболее типичные примеры, с которыми вам придется иметь дело.

Полное объединение двух веток

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

Как применить в этом случае svn merge? Помните о том, что эта команда сравнивает два дерева и применяет различия к рабочей копии. Следовательно, для того, чтобы было к чему применять изменения, необходимо иметь рабочую копию главной линии разработки. Будем считать, что такая, полностью обновленная копия у вас либо уже есть, либо вы ее только что создали в каталоге /calc/trunk.

Но какие именно два дерева нужно сравнивать? На первый взгляд ответ очевиден: нужно сравнить последнее дерево главной линии разработки с последним деревом вашей ветки. Однако такое предположение будет ошибочным. Это типичная ошибка большинства новичков! Поскольку svn merge работает так же как и svn diff, сравнение последней версии главной линии разработки и вашей ветки покажет изменения, сделанные не только в вашей ветке. Такое сравнение покажет слишком много изменений. Оно выведет не только то, что добавлялось в вашей ветке, но и то, что удалялось в главной линии разработки и не удалялось в вашей ветке.

Чтобы выделить только те изменения, которые были сделаны в вашей ветке, нужно сравнивать начальное и конечное состояния этой ветки. Посмотрев svn log, можно узнать, что ветка была создана в правке 341. В качестве конечного состояния ветки можно просто использовать правку HEAD. Это значит, что вам нужно сравнить правки 341 и HEAD каталога с веткой и применить различия к рабочей копии главной линии разработки.

[Подсказка] Подсказка

Для определения правки, в которой была создана ветка («базовой» правки ветки), удобно использовать параметр --stop-on-copy команды svn log. Обычно эта команда показывает все изменения, сделанные в ветке, включая те, которые были сделаны до создания ветки. Поэтому вы будете видеть и историю главной линии разработки. Параметр --stop-on-copy остановит вывод лог-сообщений, как только svn log обнаружит факт копирования или переименования целевого объекта.

$ svn log --verbose --stop-on-copy \
          http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | user | 2002-11-03 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines
Changed paths:
   A /calc/branches/my-calc-branch (from /calc/trunk:340)

$

Как и ожидалось, последней строчкой эта команда выведет ту правку, в которой в результате копирования был создан каталог my-calc-branch.

Теперь мы можем завершить объединение веток:

$ cd calc/trunk
$ svn update
At revision 405.

$ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn status
M   integer.c
M   button.c
M   Makefile

# ...examine the diffs, compile, test, etc...

$ svn commit -m "Merged my-calc-branch changes r341:405 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 406.

Еще раз обратите внимание на то, что в лог-сообщении фиксации очень точно указан диапазон правок, которые были объединены с главной линией разработки. Никогда не забывайте этого делать, потому что это очень важная информация, которая понадобиться вам позже.

Предположим, что на следующей неделе вы решили продолжить работу над своей веткой, чтобы доработать новый функционал или исправить еще несколько ошибок. Теперь правка HEAD хранилища имеет номер 480, и вы готовы еще раз объединить свою личную копию с главной линией разработки. Однако, как уже было сказано в разделе «Как правильнее всего использовать слияние», ранее объединенные изменения не стоит объединять повторно. Объединению подлежат только «новые» изменения, появившиеся с момента последнего объединения. Сложность заключается в том, чтобы выделить эти новые изменения.

Первым шагом будет запуск svn log для главной линии разработки, в результате чего мы узнаем время последнего объединения с веткой:

$ cd calc/trunk
$ svn log
…
------------------------------------------------------------------------
r406 | user | 2004-02-08 11:17:26 -0600 (Sun, 08 Feb 2004) | 1 line

Merged my-calc-branch changes r341:405 into the trunk.
------------------------------------------------------------------------
…

Ага! Все изменения ветки, сделанные между правками 341 и 405, уже были объединены с главной линией разработки в правке 406. Поэтому сейчас нам нужно взять только изменения, выполненные после этого. Для этого мы будем сравнивать правки 406 и HEAD.

$ cd calc/trunk
$ svn update
At revision 480.

# We notice that HEAD is currently 480, so we use it to do the merge:

$ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn commit -m "Merged my-calc-branch changes r406:480 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 481.

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

Отмена изменений

Еще одной типичной задачей для svn merge является откат ранее сделанных изменений. Предположим, работая в копии /calc/trunk, вы вдруг выясняете, что изменения файла integer.c, сделанные в правке 303, были совершенно ошибочными. Вы можете воспользоваться командой svn merge для «отмены» изменений в своей рабочей копии, после чего зафиксировать локальные изменения в хранилище. Для этого нужно всего лишь указать различия в обратном порядке:

$ svn merge -r 303:302 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

$ svn diff
…
# verify that the change is removed
…

$ svn commit -m "Undoing change committed in r303."
Sending        integer.c
Transmitting file data .
Committed revision 350.

Правку хранилища можно рассматривать как группу изменений (некоторые системы управления версиями называют это набором изменений). Используя параметр -r, можно попросить svn merge применить к рабочей копии набор изменений или целый диапазон наборов изменений. В нашем примере с отменой изменений мы просим svn merge применить к рабочей копии набор изменений #303 в обратном направлении.

Обратите внимание, что подобный откат изменений ничем не отличается от остальных операций, выполняемых с помощью svn merge. Поэтому необходимо с помощью svn status и svn diff убедиться в том, что результат соответствует ожиданиям, а затем, используя svn commit, отправить финальную версию в хранилище. После фиксации этот конкретный набор изменений больше не будет отражен в правке HEAD.

Давайте, однако, подумаем: отменяется ли на самом деле предыдущая фиксация? Изменения продолжают существовать в правке 303. И если кто-то создаст рабочую копию проекта calc между правками 303 и 349, он ведь все равно получит ошибочные изменения?

Безусловно, это так. Говоря об «удалении» изменений, мы имеем в виду их удаление из правки HEAD. Исходные изменения по-прежнему останутся в истории хранилища. Чаще всего это можно рассматривать даже как положительный момент. В любом случае, большинство пользователей интересует только HEAD-правка проекта. Однако, возможны ситуации, когда необходимо полностью удалить последствия фиксации. (Например, если кто-то случайно зафиксировал конфиденциальный документ.) Сделать это будет непросто, так как архитектура Subversion намеренно исключает возможность потери информации. Правки представляют собой не меняющиеся деревья файлов, основанные друг на друге. Удаление правки из хранилища может вызвать эффект домино, потащив за собой все последующие правки и повредив все рабочие копии. [26]

Восстановление удаленных элементов

Отличным свойством системы управления версиями является то, что информация никогда не теряется. При удалении файла или каталога элемент исключается из правки HEAD, но продолжает существовать в более ранних правках. Новые пользователи очень часто спрашивают: «Как мне вернуть назад свой файл или каталог?»

Для начала было бы неплохо определиться, какой именно элемент мы пытаемся восстановить. Для этого полезно представить все объекты хранилища как точки в двумерной системе координат. Первой координатой будет отдельное дерево правок, в второй — путь в этом дереве. Таким образом, каждая версия файла или каталога может быть задана парой координат.

В отличие от CVS, Subversion не имеет каталога Attic[27], поэтому для определения искомой пары координат восстанавливаемого объекта нужно вызвать svn log. Лучше всего запустить svn log --verbose в каталоге, который содержал удаленный объект. Параметр --verbose покажет для каждой правки список измененных элементов — в этом случае вам останется только найти ту правку, в которой файл или каталог были удалены. Это можно сделать визуально или обработав вывод команды каким-нибудь инструментом (например, утилитой grep или просто запустив поиск в редакторе).

$ cd parent-dir
$ svn log --verbose
…
------------------------------------------------------------------------
r808 | joe | 2003-12-26 14:29:40 -0600 (Fri, 26 Dec 2003) | 3 lines
Changed paths:
   D /calc/trunk/real.c
   M /calc/trunk/integer.c

Added fast fourier transform functions to integer.c.
Removed real.c because code now in double.c.
…

В этом примере предполагается, что вы ищете удаленный файл real.c. Просмотрев логи родительского каталога, вы можете заметить, что этот файл был удален в правке 808. Следовательно, самая последняя версия этого файла была в правке, предшествующей удалению. Вывод: необходимо из правки 807 восстановить путь /calc/trunk/real.c.

Поиск — непростая задача. Теперь, когда нам известно, что именно нужно восстановить, есть две варианта действий.

Во-первых, мы можем посредством svn merge применить правку 808 «в обратном направлении». (Как отменять изменения, мы уже рассматривали, см. «Отмена изменений».) Это приведет к повторному добавлению файла real.c в форме локального изменения. Файл будет запланирован для добавления, и после фиксации будет снова присутствовать в HEAD.

Однако, в данном примере это не самое лучшее решение. Повторное применение правки 808 не только добавит файл real.c. Лог-сообщение показывает, что также будут отменены некоторые изменения в integer.c, что весьма нежелательно. Конечно, можно выполнить обратное слияние с правкой 808, а затем отменить (svn revert) локальные изменения в integer.c, но такой подход плохо масштабируется. Что, если в правке 808 изменилось 90 файлов?

При другом, более аккуратном подходе svn merge вообще не используется, а вместо этого применяется команда svn copy. Просто скопируйте объект, заданный «парой координат» (правка и путь), из хранилища в рабочую копию:

$ svn copy --revision 807 \
           http://svn.example.com/repos/calc/trunk/real.c ./real.c

$ svn status
A  +   real.c

$ svn commit -m "Resurrected real.c from revision 807, /calc/trunk/real.c."
Adding         real.c
Transmitting file data .
Committed revision 1390.

Знак "плюс" в столбце статуса показывает, что элемент не просто запланирован для добавления, а запланирован для добавления «с историей». Subversion запоминает, откуда он был скопирован. В будущем вызов svn log для этого файла будет пересекать точку восстановления файла и прослеживать всю историю, предшествующую правке 807. Другими словами, новый файл real.c на самом деле не является новым; он является прямым потомком исходного файла, который был удален.

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

Типовые приемы использования веток

Управление версиями чаще всего используется при разработке программного обеспечения, поэтому здесь мы вкратце рассмотрим два наиболее часто используемых командами программистов приема ветвления/слияния. Если вы не используете Subversion для разработки программного обеспечения, можете пропустить этот раздел. Если вы — разработчик программного обеспечения, впервые использующий контроль версий, уделите этому разделу пристальное внимание, поскольку опытные разработчики считают использование данных приемов хорошим стилем. Такие приемы не являются специфичными для Subversion; они применимы к любой системе управления версиями. Однако, здесь мы взглянем на них с позиций Subversion.

Ветки релизов

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

Здесь-то и может помочь контроль версий. Типичная процедура выглядит примерно так:

  • Разработчики фиксируют все новое в главную линию разработки. Каждодневные изменения фиксируются в /trunk: новая функциональность, исправление ошибок и тому подобное.

  • Главная линия разработки копируется в ветку «релиза». Когда команда разработчиков решает, что программа готова к выпуску (скажем, к релизу 1.0), тогда /trunk копируется, например, в /branches/1.0.

  • Группы продолжают работать параллельно. Одна группа начинает всестороннее тестирование ветки релиза, в то время как вторая группа продолжает работу (скажем, над версией 2.0) в /trunk. Если находятся ошибки в какой-либо из версий, исправления портируются по необходимости в одну или другую сторону. В какой-то момент этот процесс останавливается. Ветка «замораживается» для окончательной проверки перед релизом.

  • На основе ветки создается метка и выпускается релиз. Когда тестирование завершено, /branches/1.0 копируется в /tags/1.0.0 как справочный снимок. Метка запаковывается и отправляется пользователям.

  • Ветка продолжает поддерживаться. По мере продвижения работы над /trunk для версии 2.0, исправления ошибок продолжают портироваться из /trunk в /branches/1.0. Когда будет накоплено определенной количество исправлений, руководство может решить сделать релиз 1.0.1: /branches/1.0 копируется в /tags/1.0.1, метка пакуется и выпускается.

По мере развития программы эти этапы повторяются: когда работа над 2.0 будет завершена, создается новая ветка релиза 2.0, тестируется, создается метка и в последствии выпускается релиз. После нескольких лет в хранилище будет находиться определенное количество веток релизов, находящихся в режиме «сопровождения» и определенное количество меток, отражающих последние выпущенные ветки.

Функциональные ветки

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

Опять же, правила проекта относительно определения момента, когда требуется создание функциональной ветки могут быть разными. Некоторые проекты вообще никогда не используют функциональные ветки: все фиксируется в /trunk. Преимущества такой системы в ее простоте — никому не нужно учиться делать ветки или объединения. Недостатком является то, что главная линия разработки часто не стабильна или не пригодна к использованию. В других проектах ветки используют по-другому: ни одного изменения не фиксируют в главной линии разработки напрямую. Даже для самых простых изменений создается краткосрочная ветка, внимательно анализируется и объединяется с главной линией. После чего ветка удаляется. Ценой огромных накладных расходов, такая система гарантирует исключительную стабильность и пригодность к использованию главной линии разработки в любой момент времени.

Большинство проектов использует что-то среднее. Как правило, все время контролируя, что /trunk компилируется и проходит регрессивные тесты. Функциональная ветка требуется только тогда, когда изменение требует большого количества дестабилизирующих фиксаций. Хорошим способом проверки является постановка такого вопроса: если разработчик работал несколько дней изолировано, а затем за один раз зафиксировал большое изменение (притом, что /trunk не будет дестабилизирован) будет ли сложно отследить это изменение? Если ответ на этот вопрос «да», то тогда изменение должно разрабатываться в функциональной ветке. По мере того, как разработчик последовательно фиксирует изменения в ветку, они могут легко отслеживаться другими участниками.

Напоследок, рекомендация, как по ходу работы лучше всего сохранять функциональную ветку «синхронизированной» с главной линией разработки. Как мы уже говорили, рискованно работать над веткой в течение недель или месяцев; изменения в главной линии будут продолжаться, и настанет момента, когда две линии разработки станут отличаться так сильно, что попытка объединить ветку обратно с главной линией разработки может стать ночным кошмаром.

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

Начиная с какого-то момента вы будете готовы объединить «синхронизированную» функциональную ветку с главной линией разработки. Для этого начните с завершающего объединения последних изменений из главной линии разработки с веткой. После чего, последняя версия ветки и главной линии будут абсолютно идентичны, за исключением ваших изменений в ветке. Теперь объединение заключается в сравнении ветки с главной линией разработки:

$ cd trunk-working-copy

$ svn update
At revision 1910.

$ svn merge http://svn.example.com/repos/calc/trunk@1910 \
            http://svn.example.com/repos/calc/branches/mybranch@1910
U  real.c
U  integer.c
A  newdirectory
A  newdirectory/newfile
…

Сравнивая правку HEAD главной линии разработки и правку HEAD ветки, определяется дельта, которая представляет собой только изменения сделанные в ветке; обе линии разработки уже содержат все изменения из главной линии.

Другим способом представления этого приема является то, что еженедельная синхронизация ветки аналогична запуску svn update в рабочей копии, в то время как окончательное объединение аналогично запуску из рабочей копии svn commit. В конце концов, что же такое рабочая копия если не миниатюрная личная ветка? Это такая ветка, которая способна хранить одно изменение в каждый момент времени.



[26] Однако, разработчики Subversion в будущем планируют реализовать команду svnadmin obliterate, которая позволит полностью удалять информацию. А пока за возможным решением проблемы обратитесь к разделу «svndumpfilter».

[27] Из-за того, что CVS не версионирует деревья, она создает область Attic для каждого каталога хранилища как способ сохранения информации об удаленных файлах.