Копирование изменений между ветками

Теперь вы и Салли работаете над параллельными ветками проекта: вы — над своей собственной веткой, а Салли — над главной линией разработки (каталог trunk).

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

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

Вместо этого вы и Салли можете продолжать делиться изменениями по ходу работы. Вы можете решать вплоть до отдельного изменения, стоит ли им делиться — Subversion предоставляет возможность выборочного «копирования» изменений между ветками. А тогда, когда вы полностью закончите работу со своей веткой, вы можете скопировать обратно в основную ветку все изменения полностью.

Копирование отдельных изменений

В предыдущем разделе мы отметили, что и вы, и Салли, одновременно, в разных ветках вносите изменения в файл integer.c. Если посмотреть на лог-сообщение Салли для правки 344, вы увидите, что она исправила несколько орфографических ошибок. Конечно же, в вашей копии этого файла эти ошибки остались. Не исключено, что ваши будущие изменения этого файла коснутся областей, содержащих эти орфографические ошибки, и таким образом вы получите несколько потенциальных конфликтов при последующем объединении вашей ветки. Поэтому лучше получить изменения от Салли сейчас, до того, как вы начнете вплотную работать с этой частью файла.

Настал момент воспользоваться командой svn merge. Эта команда, оказывается, является очень близким родственником команды svn diff (о которой вы читали в Глава 2, Экскурсия по Subversion). Обе команды способны сравнивать любые два объекта в хранилище и показывать различия. Например, вы можете попросить svn diff показать все изменения, сделанные Салли в правке 344:

$ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk

Index: integer.c
===================================================================
--- integer.c (revision 343)
+++ integer.c (revision 344)
@@ -147,7 +147,7 @@
     case 6:  sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break;
     case 7:  sprintf(info->operating_system, "Macintosh"); break;
     case 8:  sprintf(info->operating_system, "Z-System"); break;
-    case 9:  sprintf(info->operating_system, "CPM"); break;
+    case 9:  sprintf(info->operating_system, "CP/M"); break;
     case 10:  sprintf(info->operating_system, "TOPS-20"); break;
     case 11:  sprintf(info->operating_system, "NTFS (Windows NT)"); break;
     case 12:  sprintf(info->operating_system, "QDOS"); break;
@@ -164,7 +164,7 @@
     low = (unsigned short) read_byte(gzfile);  /* read LSB */
     high = (unsigned short) read_byte(gzfile); /* read MSB */
     high = high << 8;  /* interpret MSB correctly */
-    total = low + high; /* add them togethe for correct total */
+    total = low + high; /* add them together for correct total */

     info->extra_header = (unsigned char *) my_malloc(total);
     fread(info->extra_header, total, 1, gzfile);
@@ -241,7 +241,7 @@
      Store the offset with ftell() ! */

   if ((info->data_offset = ftell(gzfile))== -1) {
-    printf("error: ftell() retturned -1.\n");
+    printf("error: ftell() returned -1.\n");
     exit(1);
   }

@@ -249,7 +249,7 @@
   printf("I believe start of compressed data is %u\n", info->data_offset);
   #endif

-  /* Set postion eight bytes from the end of the file. */
+  /* Set position eight bytes from the end of the file. */

   if (fseek(gzfile, -8, SEEK_END)) {
     printf("error: fseek() returned non-zero\n");

Команда svn merge ведет себя практически идентично. Но вместо вывода различий на терминал она применяет их к рабочей копии в виде локальных изменений:

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

$ svn status
M  integer.c

Вывод команды svn merge показывает, что к вашей копии integer.c был применен патч. Теперь она содержит изменения Салли — они были «скопированы» из главной линии разработки в вашу рабочую копию, вашу собственную ветку, и теперь существуют в виде локальных изменений. С этого момента вы можете просмотреть локальные изменения и убедиться в том, что они корректны.

Возможна ситуация, когда не все будет так хорошо и integer.c окажется в состоянии конфликта. Тогда вам будет необходимо разрешить конфликт обычным путем (см. Глава 2, Экскурсия по Subversion), либо, если вы придете к мнению, что объединение было плохой идеей, просто отказаться от него, отменив локальные изменения командой svn revert.

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

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

$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk."
Sending        integer.c
Transmitting file data .
Committed revision 360.

Как вы увидите в последующих разделах, очень важно следовать этой «хорошей практике».

Небольшое предупреждение: несмотря на то, что svn diff и svn merge по сути очень похожи, во многих случаях они используют разный синтаксис. Обязательно прочтите об этом в Глава 9, Полное справочное руководство по Subversion, или спросите у svn help. Например, svn merge требует в качестве целевого объекта путь в рабочей копии, то есть место, где ей нужно применить изменения структуры файлов. Если целевой объект не указан, предполагается, что делается попытка выполнить одну из следующих операций:

  1. объединение изменений каталога с вашим текущим рабочим каталогом;

  2. объединение изменений в конкретном файле с файлом, имеющим то же имя в текущем рабочем каталоге.

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

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

$ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk my-calc-branch
U   my-calc-branch/integer.c

Ключевые идеи, стоящие за слиянием

Теперь, после знакомства с примерами использования svn merge, настала пора разобраться в них поглубже. Если вы чувствуете неуверенность в том как, собственно, работает слияние, то вы в этом вы не одиноки. Многие пользователи (особенно те, для которых управление версиями в новинку) поначалу путаются в правильности записи этой команды и в том, как и когда эту функцию следует использовать. Отбросьте страх — на самом деле эта команда намного проще, чем вы думаете! Понять, как именно ведет себя svn merge, очень просто.

В ступор вводит, главным образом, название команды. Термин «слияние» как бы указывает на то, что ветки соединяются вместе, или происходит какое-то волшебное смешивание данных. На самом деле это не так. Пожалуй, этой команде лучше бы подощло название svn diff-and-apply, поскольку это всё, что в результате происходит: сравниваются два файловых дерева хранилища, а различия переносятся в рабочую копию.

Команда принимает три аргумента:

  1. Начальное дерево хранилища (как правило, называемое левой частью при сравнении),

  2. Конечное дерево хранилища (как правило называемое правой частью при сравнении),

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

Когда эти три аргумента указаны, производится сравнение двух деревьев, а полученные различия применяются к целевой рабочей копии в виде локальных изменений. После выполнения этой команды результат будет так же, как в случае, если бы вы вручную редактировали файлы или многократно выполняли команды svn add или svn delete. Если результат вас устраивает, его можно зафиксировать. Если результат вас не устраивает, просто отмените (svn revert) все сделанные изменения.

Синтаксис svn merge позволяет указывать эти три аргумента довольно гибко. Вот несколько примеров:

      
$ svn merge http://svn.example.com/repos/branch1@150 \
            http://svn.example.com/repos/branch2@212 \
            my-working-copy

$ svn merge -r 100:200 http://svn.example.com/repos/trunk my-working-copy

$ svn merge -r 100:200 http://svn.example.com/repos/trunk

В первом варианте все три аргумента указаны явно — каждое дерево задано в форме URL@REV и указана целевая рабочая копия. Второй вариант может использоваться для краткости записи в ситуациях, когда сравниваются две разных правки по одному и тому же URL. Последний вариант демонстрирует возможность не указывать целевую рабочую копию, при этом по умолчанию используется текущий каталог.

Как правильнее всего использовать слияние

Ручной контроль слияния

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

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

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

Что это означает для вас как пользователя? Это означает, что до того момента, пока у Subversion не появится этой функции, вам придется контролировать слияние информации самостоятельно. Лучшим местом для этого является лог-сообщение. Как было показано в предыдущих примерах, рекомендуется, чтобы в лог-сообщении был указан конкретный номер правки (или диапазон правок), которые были слиты в вашу ветку. В этом случае вы сможете впоследствии просмотреть, какие изменения уже содержит ваша ветка, запусти команду svn log. Это позволит быть осторожнее при последующих запусках команды svn merge и избежать пересечения с уже портированными изменениями.

В следующем разделе мы на примерах рассмотрим эту методику в действии.

Предварительный просмотр результатов слияния

Поскольку результатом слияния являются локальные изменения, такая операция не является опасной. Допустив при слиянии ошибку, можно просто отменить изменения (svn revert) и попробовать еще раз.

Однако, возможна ситуация, когда рабочая копия уже содержит локальные изменения. Изменения, примененные при слиянии, будут смешаны с уже существующими, и запуск svn revert нас больше не устроит. Разделить два множества изменений будет невозможно.

В такой ситуации лучше попробовать спрогнозировать или проверить результат слияния до того, как оно произойдет. Проще всего запустить для этого svn diff с теми же аргументами, что и для svn merge. Мы уже показывали это в первом примере объединения. Другим способом предварительного просмотра может служить вызов команды слияния с опцией --dry-run:

$ svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
#  nothing printed, working copy is still unchanged.

Опция --dry-run позволяет не применять локальные изменения к рабочей копии. Она только показывает статусы, которые имели бы файлы при реальном объединении. Это полезно для получения «сводной» информации о потенциальном слиянии в тех случаях, когда запуск svn diff выдает слишком много подробностей.

Конфликты при слиянии

Так же как и svn update, команда svn merge внедряет изменения в рабочую копию. Следовательно, она тоже может создавать конфликты. Однако конфликты, порождаемые svn merge, имеют определенные отличия, и поэтому мы их сейчас рассмотрим подробнее.

Вначале предположим, что рабочая копия не имеет локальных изменений. При обновлении (svn update) рабочей копии до конкретной правки отправляемые сервером изменения будут всегда «без проблем» внедряться в рабочую копию. Сервер создает дельту, сравнивая два дерева: виртуальный снимок рабочей копии и дерево файлов, которое вас интересует. Учитывая то, что левая часть сравнения полностью эквивалентна тому, что вы уже имеете, дельта гарантированно правильно конвертирует рабочую копию в правую часть сравнения.

Однако svn merge не дает такой гарантии, и может вести себя более непредсказуемо: пользователь может запросить сервер сравнить любые два дерева файлов, даже такие, которые не имеют отношения к рабочей копии! Из этого следует множество потенциальных человеческих ошибок. Иногда пользователи будут сравнивать два ошибочных дерева, создавая дельту, которая не сможет правильно внедриться. svn merge будет пытаться внедрить различия по максимуму, но иногда это будет невозможно. Так же как команда patch в Unix иногда жалуется на «неудачные попытки» объединения, svn merge будет жаловаться на «пропущенные цели»:

$ svn merge -r 1288:1351 http://svn.example.com/repos/branch
U  foo.c
U  bar.c
Skipped missing target: 'baz.c'
U  glub.c
C  glorb.h

$

Возможно, что в предыдущем примере файл baz.c существует в обоих сравниваемых снимках ветки и Subversion пытается применить результирующую дельту для того, чтобы изменить содержимое файла, однако в рабочей копии файл отсутствует. В любом случае сообщение «skipped» означает, что скорее всего пользователь ошибся при указании деревьев для сравнения — то есть это классическая ошибка оператора. Если это так, проще всего рекурсивно отменить все изменения, сделанные при слиянии (svn revert --recursive), сразу же после этого удалить все неверсионированные файлы и каталоги и повторно запустить svn merge с другими параметрами.

Обратите внимание на то, что в предыдущем примере в файле glorb.h возник конфликт. Откуда же он мог взяться, если ранее мы договорились, что рабочая копия не имеет локальных изменений? Опять же, пользователь мог запустить svn merge для выделения и применения к рабочей копии какой то старой дельты. В результате такая дельта может содержать изменения, которые нельзя внедрить в рабочий файл без появления проблем, даже если он не имеет локальных изменений.

Еще одно небольшое отличие между svn update и svn merge заключается в названиях файлов, создаваемых при конфликтах. В разделе «Разрешение конфликтов (при слиянии с чужими изменениями)» мы говорили о том, что при обновлении создаются файлы с названиями filename.mine, filename.rOLDREV и filename.rNEWREV. А svn merge в конфликтной ситуации создает три файла с названиями filename.working, filename.left и filename.right. Термины «left» и «right» указывают здесь на две стороны сравнения, то есть на используемые при сравнении деревья. Это разделение используемых названий поможет вам отличать конфликты, возникшие в результате обновления, от конфликтов, возникших в результате слияния.

Учитывать или игнорировать происхождение

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

Предположим, что фиксируется правка 100, в которой изменяется файл foo.c. В этом случае файл foo.c@99 является предком файла foo.c@100. С другой стороны, можно допустить, что в правке 101 вы фиксируете удаление foo.c, а затем в правке 102 добавляете новый файл с таким же именем. В таком случае может показаться, что файлы foo.c@99 и foo.c@102 имеют отношение друг к другу (у них одинаковый путь), однако на самом деле они являются полностью независимыми объектами хранилища. Они не имеют ни общей истории, ни общих «предков».

Мы обращаем на это ваше внимание, чтобы указать на важные различия между svn diff и svn merge. Первая команда игнорирует происхождение, в то время как вторая его учитывает. Например, если попросить svn diff сравнить правки 99 и 102 файла foo.c вы увидите построчное сравнение; команда diff слепо сравнивает два пути. А вот если вы попросите svn merge сравнить те же объекты, то Subversion предупредит вас о том, что они не связаны друг с другом и сначала попытается удалить старый файл, а затем добавить новый; вывод команды покажет удаление с последующим добавлением:

D  foo.c
A  foo.c

В большинстве случаев при слиянии сравниваются деревья, имеющие родственную связь и по умолчанию svn merge рассчитывает на это. Однако иногда вам будет нужно, чтобы команда merge сравнила два независимых дерева файлов. Например, вы могли импортировать два дерева, содержащие исходный код релизов программных проектов сторонних поставщиков (см. «Vendor branches»). Если попросить svn merge сравнить два эти дерева, вы увидите, что первое дерево будет полностью удалено, а затем будет полностью добавлено второе!

В подобных ситуациях нужно, чтобы команда svn merge выполняла сравнение, учитывая только пути, и не обращала внимание на отношения между файлами и каталогами. Добавьте опцию --ignore-ancestry при запуске команды слияния, после чего она будет вести себя аналогично svn diff. (И наоборот, опция --notice-ancestry заставит svn diff работать подобно команде merge.)



[25] В будущем проект Subversion планирует использовать (или изобрести) расширенный формат представления различий, который будет передавать изменения в структуре дерева файлов и свойств.