Глава 3. Экскурсия по Mercurial: слияние результатов работы

Содержание

3.1. Слияние потоков работы
3.1.1. Головная ревизия
3.1.2. Выполнение слияния
3.1.3. Фиксация результатов слияния
3.2. Слияние конфликтующих изменений
3.2.1. Использование графического инструмента слияния
3.2.2. Рабочий пример
3.3. Упрощение последовательности pull-merge-commit
3.4. Переименование, копирование и слияние

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

3.1. Слияние потоков работы

Слияние — это основная часть работы с инструментами распределённого контроля версий. Вот несколько случаев, когда возникает необходимость объединить работу:

  • Элис и Боб имеют собственные репозитории проекта, над которым они вместе работают. Элис пофиксила ошибку в своем репозитории, а Боб добавил новую фичу в своём. Они хотят чтобы общий репозиторий содержал и багфикс и новую фичу.

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

Поскольку слияние очень частая операция, Mercurial имеет простые средства её осуществления. Давайте рассмотрим процесс слияния. Начнем с еще одного клонирования репозитория (заметили насколько часто мы это делаем?) и внесения изменений в него.

$ cd ..
$ hg clone hello my-new-hello
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd my-new-hello
# Make some simple edits to hello.c.
$ my-text-editor hello.c
$ hg commit -m 'A new hello for a new day.'

Теперь у нас есть две копии hello.c c разным содержимым. История изменения этих двух репозиториев тоже различается, как показано на Рисунок 3.1, «Расхождение историй репозиториев my-hello и my-new-hello». Вот копия нашего файла из одного репозитория:

$ cat hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("once more, hello.\n");
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

А здесь немного отличающаяся копия из другого хранилища:

$ cat ../my-hello/hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

Рисунок 3.1. Расхождение историй репозиториев my-hello и my-new-hello

XXX add text

Мы уже знаем, что получение изменений из репозитория my-hello не изменит состояния рабочего каталога.

$ hg pull ../my-hello
pulling from ../my-hello
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

При этом команда hg pull говорит что-то о «головах» (heads), мол их стало на одну больше (+1 heads).

3.1.1. Головная ревизия

Напомним, что каждая ревизия в Mercurial, имеет родительскую ревизию. Если у ревизии есть родитель, мы называем её потомком. У головной (head) ревизии нет потомков. Главная (tip) ревизия тоже головная, потому что самая свежая ревизия в хранилище не может иметь потомков. Случаются моменты, когда репозиторий может содержать более одной головной ревизии.

Рисунок 3.2. Содержимое хранилища my-new-hello после получения изменений из my-hello

XXX add text

На Рисунок 3.2, «Содержимое хранилища my-new-hello после получения изменений из my-hello» показан результат получения изменений из my-hello в my-new-hello. История, уже имеющаяся в my-new-hello не затронута, но добавлена новая ревизия. По сравнению с Рисунок 3.1, «Расхождение историй репозиториев my-hello и my-new-hello», видно, что ID ревизии остался прежним, а номер ревизии изменён. (Это, кстати, хороший пример, почему использование номеров ревизий при обсуждении наборов изменений не надёжно) Посмотреть головы в хранилище позволяет команда hg heads.

$ hg heads
changeset:   6:c3e3be994861
tag:         tip
parent:      4:2278160e78d4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:17 2012 +0000
summary:     Added an extra line of output

changeset:   5:2908f9fcaad4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:23 2012 +0000
summary:     A new hello for a new day.

3.1.2. Выполнение слияния

Что произойдёт, если мы попытаемся выполнить обычную команду hg update для обновления до новой головной ревизии?

$ hg update
abort: crosses branches (merge branches or update --check to force update)

Mercurial говорит нам, что команда hg update не выполняет слияния. Она думает, что мы ожидаем слияния, и не обновит рабочий каталог, если мы не принудим её к этому. (В данном случае, принудительное обновление с помощью hg update -C удалит все не сохраненные изменения в рабочем каталоге).

Для объединения двух голов мы воспользуемся командой hg merge.

$ hg merge
merging hello.c
0 files updated, 1 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)

Произошло слияние содержимого файла hello.c. Это привело к обновлению рабочего каталога — он теперь содержит изменения от обоих голов, что будет отражено в выводе hg parents и в содержании файла hello.c.

$ hg parents
changeset:   5:2908f9fcaad4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:23 2012 +0000
summary:     A new hello for a new day.

changeset:   6:c3e3be994861
tag:         tip
parent:      4:2278160e78d4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:17 2012 +0000
summary:     Added an extra line of output

$ cat hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("once more, hello.\n");
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

3.1.3. Фиксация результатов слияния

Каждый раз, когда мы делаем слияние, hg parents показывает двух родителей пока мы не закрепим результат командой hg commit.

$ hg commit -m 'Merged changes'

Теперь у нас есть новая главная ревизия. Заметим, что обе бывшие головы теперь родители. Это те же ревизии, что раньше отображались командой hg parents.

$ hg tip
changeset:   7:45b2a02b6844
tag:         tip
parent:      5:2908f9fcaad4
parent:      6:c3e3be994861
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:24 2012 +0000
summary:     Merged changes

На Рисунок 3.3, «Рабочий каталог и репозиторий во время и после совершения слияния» вы можете увидеть представление того, что происходит с рабочим каталогом при слиянии, и как это влияет на хранилище, когда происходит коммит. Во время слияния, рабочий каталог состоял из двух родительских ревизий, и они стали родителями новой ревизии.

Рисунок 3.3. Рабочий каталог и репозиторий во время и после совершения слияния

XXX add text

Иногда мы говорим о слиянии по сторонам: в левой части первый родитель, указанный в выводе hg parents, а в правой части — второй. Если до слияния рабочий каталог был таким как в ревизии 5, то ревизия будет с левосторонним слиянием.

3.2. Слияние конфликтующих изменений

Большая часть слияний проста, но иногда при слиянии возможны конфликты, когда участники изменили одинаковые части одного и того же файла. Если изменения не идентичны, то произойдет конфликт и вам придется решать, как согласовать изменения во что-то связное.

Рисунок 3.4. Конфликт изменений в документе

XXX add text

Рисунок 3.4, «Конфликт изменений в документе» показывает пример конфликта двух изменений в документе. Мы начали с одной версии файла, затем сделали несколько изменений, в то время, как кто-то другой также изменял этот текст. Наша задача в разрешении конфликта изменений — решить, как должен выглядеть окончательный вариант файла.

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

Можно указать Mercurial использовать определённую программу, установив переменную окружения HGMERGE со значением имени необходимой программы.

3.2.1. Использование графического инструмента слияния

Мой любимый графический инструмент слияния это kdiff3, и его я буду использовать для описания возможностей, которые являются общими для графических инструментов слияния. На Рисунок 3.5, «Использование kdiff3 для слияния версий файлов» показан снимок экрана kdiff3 в работе. Выполняемое таким образом слияние называется тройственным (three-way), потому что есть три различные версии файла, интересующие нас. В инструменте сравнения верхняя часть окна поделена на три панели:

  • Слева базовая версия файла, т.е. самая последняя версия, после которой произошло разделение на те две версии, которые мы пытаемся объединить.

  • Посередине «наша» версия файла, содержащая наши изменения.

  • Справа «их» версия файла, то есть версия из ревизии, с которой мы производим слияние.

На панели снизу располагается текущий результат слияния. Наша задача — заменить весь красный текст, означающий неразрешенные конфликты, на осмысленный результат слияния «нашей» и «их» версий файла.

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

Рисунок 3.5. Использование kdiff3 для слияния версий файлов

XXX add text

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

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

3.2.2. Рабочий пример

В этом примере мы воспроизведем историю модификации файла с Рисунок 3.4, «Конфликт изменений в документе». Давайте начнем с создания пустого репозитория с базовой версией нашего документа.

$ cat > letter.txt <<EOF
> Greetings!
> I am Mariam Abacha, the wife of former
> Nigerian dictator Sani Abacha.
> EOF
$ hg add letter.txt
$ hg commit -m '419 scam, first draft'

Мы клонируем репозиторий и изменим файл.

$ cd ..
$ hg clone scam scam-cousin
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-cousin
$ cat > letter.txt <<EOF
> Greetings!
> I am Shehu Musa Abacha, cousin to the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg commit -m '419 scam, with cousin'

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

$ cd ..
$ hg clone scam scam-son
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-son
$ cat > letter.txt <<EOF
> Greetings!
> I am Alhaji Abba Abacha, son of the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg commit -m '419 scam, with son'

Создав две разных версии файла, создадим окружение, в котором можно будет произвести наше объединение.

$ cd ..
$ hg clone scam-cousin scam-merge
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-merge
$ hg pull -u ../scam-son
pulling from ../scam-son
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
not updating: crosses branches (merge branches or update --check to force update)

В данном случае, я установил такое значение HGMERGE, чтобы Mercurial использовал консольную команду merge. Она встроена во многие Unix-подобные системы. Если вы выполняете этот пример на своём компьютере, то можете себя этим не утруждать. Вы просто передадите файл графическому инструменту для слияний, что гораздо предпочтительнее.

$ export HGMERGE=merge
$ hg merge
merging letter.txt
/bin/sh: merge: command not found
merging letter.txt failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
$ cat letter.txt
Greetings!
I am Shehu Musa Abacha, cousin to the former
Nigerian dictator Sani Abacha.

Так как merge не может самостоятельно выбрать правильное из противоречащих изменений, она оставляет маркеры слияния в файле с конфликтами, обозначая наши и их строки, содержащие противоречие.

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

Если автоматическое или ручное объединение не удалось, то ничто не мешает нам самим «поправить» пострадавшие файлы и закоммитить результаты слияния:

$ cat > letter.txt <<EOF
> Greetings!
> I am Bryan O'Sullivan, no relation of the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg resolve -m letter.txt
$ hg commit -m 'Send me your money'
$ hg tip
changeset:   3:a31b9a7b1e74
tag:         tip
parent:      1:1504df74de25
parent:      2:203073c5900c
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Feb 02 14:10:25 2012 +0000
summary:     Send me your money

[Примечание] Где команда hg resolve?

Команда hg resolve была добавлена в Mercurial 1.1, выпущенный в декабре 2008 года. Если вы используете старую версию Mercurial (запустите hg version, чтобы узнать номер версии), эта команда вам недоступна. Если вы используете Mercurial версии ниже 1.1, вам следует подумать об обновлении прежде, чем пытаться решать сложные слияния.

3.3. Упрощение последовательности pull-merge-commit

Процесс слияния изменений, как говорилось выше, прост, но требует выполнения последовательности из трёх команд.

hg pull -u
hg merge
hg commit -m 'Merged remote changes'

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

Хорошо было бы, по возможности, сократить количество шагов. И действительно, Mercurial поставляется с расширением fetch, которое делает именно это.

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

Расширение fetch добавляет новую команду hg fetch. По сути, это комбинация команд hg pull -u, hg merge и hg commit. Выполнение команды начинается с получения изменении из необходимого репозитория в текущий. Если находятся изменения, добавляющие новую голову в репозиторий, то начинается слияние, затем, если слияние прошло успешно, происходит коммит результата с автоматической генерацией комментария. Если новых голов не было, расширение просто обновляет рабочую директорию.

Подключить расширение fetch просто: откройте в текстовом редакторе файл .hgrc, и добавьте в секцию extensions строку «fetch=», либо сначала создайте такую секцию.

[extensions]
fetch =

Обычно с правой стороны от «=» указывается местоположение расширения, но так как расширение fetch входит в стандартный пакет установки, Mercurial знает, где его искать.

3.4. Переименование, копирование и слияние

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

Mercurial свободно поддерживает такого рода сложные изменения, при условии, что мы сообщаем ему о том, что делаем. Если мы хотим переименовать файл, мы должны использовать команду hg rename[2]. Команда переименует его, так что Mercurial будет знать что делать позже при слиянии.

Мы расскажем об использовании этих команд более подробно в Раздел 5.3, «Копирование файлов».



[2] Если вы пользователь Unix, вы будете рады узнать, что команда hg rename может быть сокращена, как hg mv.