Продолжение, см. начало: Mercurial: введение в распределённые системы контроля версий
Одним из несомненных плюсов меркуриала является его интерфейс, который покажется знакомым каждому, кто хоть какое-то время работал с subversion в командной строке. Естественно, что отличий хватает (сам принцип работы совершенно иной), но наиболее часто употребляемые команды - очень похожи. Рассматривать я буду только командную строку - во-первых, я ею пользуюсь сам, во-вторых, ни один GUI не подходит и близко по возможностям, и, в-третьих, она значительно ближе “к телу” - проще понять, что, как и почему так работает.
Базовые принципы
Сразу в двух словах опишу модель данных в меркуриале (и большинстве других DVCS) - она несколько отличается от привычной по централизованным системам.
Ревизия (changeset) - это сущность, описывающая изменения в каких-либо файлах. Эта сущность хранит информацию о своём авторе, времени создания, изменениях в файлах (естественно ;) и о родительских ревизиях (которых может быть одна в случае обычной ревизии и две в случае слияния). Кстати, подсчёт 40-цифрового 16-ричного sha1-хеша, которым идентифицируется ревизия, учитывает все эти значения - таким образом каждая ревизия идентифицирует не только себя, но и всю свою историю.
Эти ревизии (благодаря наличию у себя родителей) организовывают ориентированный ациклический граф (от ревизии 0 и до существующих голов), который может разветвиться в любом месте.
Головы - это ревизии, у которых (ещё) нет детей, конечные точки графа.
При этом существуют две специальные ревизии - null (ревизия-родитель самой первой ревизии под номером 0) и tip (самая последняя ревизия, в случае наличия нескольких голов выбирается в зависимости от обстоятельств).
Основы работы
Скачать hg для Windows (под никсами, я думаю, каждый и так знает, как
пользоваться менеджером софта) можно
здесь, в довесок к этому
дистрибутиву идёт GUI, которым вроде бы даже можно пользоваться. :)
Сразу упомяну, чтобы не забыть, о необходимости настройки меркуриала.
Для этого надо отредактировать конфигурационный файл, который находится
в ~/.hgrc
в случае *nix, и в %USERPROFILE%\mercurial.ini
в случае
Windows. Надо туда добавить пару строчек:
Я думаю, они говорят сами за себя. .hgrc
в настоящий момент - самый
обычный ini-файл, знакомый, я думаю, каждому. :)
Всё начинается с того, что мы создаём репозиторий:
hg init repo
Если посмотреть внутрь директории repo
, можно увидеть директорию .hg
- это и есть сам репозиторий, в котором хранятся ревизии и настройки данного конкретного экземпляра (репозитория, простите за каламбур ;). Начать можно с добавления файлов и коммита первой ревизии:
Теперь можно просмотреть историю репозитория с помощью hg log
:
changeset: 0:2add2e250fd2
tag: tip
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:36:39 2008 +0300
summary: Initial revision
Или склонировать его:
repoclone
- абсолютно идентичен repo
в плане наполнения, однако в
нём уже появился файл .hg/hgrc
, в котором записано, что путь по
умолчанию (для забирания и отдавания изменений) - это оригинальный
repo
:
Теперь, если мы сделаем изменения в обоих репозиториях, мы можем увидеть такую картину:
Команды incoming
и outgoing
показывают соответственно входящие (те,
которых ещё нету в локальном репозитории) и исходящие (те, которых не
хватает в удалённом репозитории) ревизии. Обратите внимание, что я не
указывал путь к удалённому репозиторию - меркуриал в этом случае берёт
его из пути по умолчанию (который можно увидеть раньше). Или ругается, в
случае его отсутствия. :)
Merge
Сложившаяся ситуация подразумевает слияние - у нас появились две ветки
разработки, значит пора применять merge
:
Что произошло? Мы вытянули (pull
) отсутствовавшую ревизию из
удалённого репозитория (получив при этом две ветки локально), посмотрели
на головы (heads
- разъяснено ниже), слили изменения и зафиксировали
это коммитом (ci
- алиас для commit
).
Некоторые пояснения: heads
- команда, которая демонстрирует наличие
двух (т.е. двух разошедшихся веток разработки). merge
сливает ветки и
говорит о том, что относительно первого родителя у нас обновился 1 файл.
Ну и tip
показывает последнюю резивию, где видно, что у неë есть два
родителя. Итого граф репозитория оказывается вот таким:
Такие вот деревья hg рисует с помощью расширения graphlog. Описание его будет в продолжении этой статьи. Естественно, есть и графические аналоги для отображения графа ревизий.
Есть один момент, на который хотелось бы обратить внимание - у ревизии
может быть максимум 2 родителя. Т.е. если у нас оказывается 3 головы,
одним мержем их слить не получится - меркуриал обратит внимание на то,
что голов много и неплохо бы явно указать, с какой из ревизий хочется
слить текущую: hg merge -r rev
.
Обмен данными по сети
Один из наиболее приятных моментов hg для меня - это использование абсолютно стандартных протоколов для обмена данными между репозиториями: http (и https), ssh или, на худой конец, аттачами в email.
При этом никаких усилий или отдельных демонов (я предполагаю, что если
хочется использовать http - какой-никакой веб-сервер уже работает :)) не
требуется. ssh работает вообще без всякой настройки, а http-часть идёт в
комплекте и требует веб-сервера, могущего cgi/fastcgi/wsgi (на выбор).
Протоколы работы по ним очень похожи - локальный и удалённый меркуриалы
обмениваются bundle’ами (далее по тексту - бандл), сжатыми файлами с
группой ревизий (должен заметить, что эффективность довольно неплоха -
бандл полного репозитория примерно в 2 раза меньше, чем его .tar.bz2
архив).
Интересный момент: по ssh меркуриал может не только вытягивать и заталкивать изменения, но и клонировать в удалённый хост:
Бывает полезно, когда сидишь за NAT’ом, да и вообще лень делать много лишних движений. :) Приятно, когда на той стороне оказывается уже настроенный hgweb и этот свежесклонированный репозиторий сразу оказывается опубликованным по http. :)
Правда, возможность работать по голому http/sftp (без меркуриала на удалённой стороне, как со статическими файлами) отсутствует, но как показывает практика соседних DVCS (и история самого hg), скорость такой работы настолько низка, что лучше даже и не пробовать.
Обмен изменениями по email в текущей инкарнации предлагает выбор из двух вариантов - мы либо шлём текстовый патч, либо бандл (описание - ниже по тексту) с нужными ревизиями. Первое позволяет просмотреть себя глазами, а второе гарантирует то, что применённые ревизии будут точно такими же, как отправленные.
Рабочий процесс
Организация рабочего процесса - это, наверное, одна из самых замечательных особенностей DVCS. Распределённая система значительно меньше ограничивает полёт фантазии разработчиков, позволяя строить процесс в соответствии с их вкусом.
Я всё-таки не буду рассказывать обо всех возможностях, что может затянуться очень на долго, а просто вкратце расскажу, как построена работа в моëм проекте (собственно, в Byteflow она построена абсолютно аналогично, не считая меньшего количества разработчиков и меньшей формализованности).
На сервере существует так называемый главный репозиторий, ничем не отличающийся от других - его просто решили считать таковым. У каждого разработчика там же, на сервере, есть собственная папка, где он может создавать (и удалять) бесконечное количество клонов репозиториев (если вдруг возникнет потребность в таковых).
В главный репозиторий может отправлять изменения ограниченное количество людей -тимлиды (если их так можно назвать, все поделены на небольшие команды по 3-4 человека), релиз менеджер и т.п. Остальные же, при достижении какого-то рабочего состояния (починил баг, реализовал какую-то возможность, и т.п.) отправляет письмо тимлиду с текстовым диффом (для просмотра) и бандлом (для применения).
При этом любой разработчик может с помощью .htaccess
(использовался
Apache) у себя в домашней директории (конечно, точнее говоря - в
~/public_html/
) может настроить свою собственную копию сайта,
работающую на основе любого из своих репозиториев.
У подобной схемы организации работы существует как минимум 3 плюса:
- В репозиторий откровенно плохой код не попадает (так как тимлид хоть как-то, но просматривает этот код);
- Обмен кода между разработчиками происходит быстро и просто, с
помощью отправления писем или использования
hg serve
(встроенный http-сервер), когда они находятся рядом; - Незаконченные, но требующие какого-то согласования возможности или баги можно легко продемонстрировать на своей собственной копии сайта, что помогает поддерживать репозиторий в более чистом виде.
Могу сказать с уверенностью, что эта схема действительно является
заметно более удачной, чем традиционная работа с svn
.
Особенности работы
Несмотря на то, что базовые команды действительно похожи на набор команд
из svn
, разница всё равно существенна. Поэтому далее я вкратце опишу
те операции, которыми приходится пользоваться достаточно часто, без
подробного расписывания каждой опции - это можно прочесть в помощи,
используя hg help command
.
Как указывать ревизии
Большинство команд (все, где это имеет смысл) имеют опцию -r
, с
помощью которой можно указать, над какой конкретно ревизией (или группой
ревизий) мы хотим произвести действие (логично, что по умолчанию -
текущая).
Идентификатором ревизии может служить либо её порядковый номер (который не является чем-то стабильным и легко может поменяться при, например, отправлении своих ревизий в другой репозиторий), либо 16-ричный хеш sha1 длиной в 40 цифр, либо тег (tag, описаны ниже). Также существует три зарезервированных имени:
null
, указывает на ревизию пустого репозитория, родительскую по отношению к ревизии 0;tip
, указывает на самую новую ревизию;.
, указывает на текущую ревизию.
Указание группы ревизий имеет синтаксис, аналогичный синтаксису python
-начало:конец
. Если начало не указано, оно равняется 0. Если не указан
конец, он равняется tip
. Если начало больше, чем конец, ревизии идут в
обратном порядке. Можно отметить, что ревизии начало
и конец
включаются в отображаемые ревизии (т.е. 3:5
- это 3, 4 и 5, в отличии
от питона, где это просто 3 и 4).
Создание ревизий и обмен ими
Как можно было заметить в описании слияния, для вытягивания новых данных
в локальный репозитория служит команда pull
. Её можно ограничить до
какой-то определённой ревизии (с помощью опции -r rev
), а можно сразу
заставить сделать update
(с помощью опции -u
) - про это написано в
следующем подразделе. Это аналог команды svn update
, или, скорее,
половина этой команды (только скачивание изменений).
Для того, чтобы отправить свои ревизии в удалённый репозиторий,
используется команда push
, которая также крайне проста в
использовании, и аналогично может ограничиваться до какой-то ревизии,
если не хочется публиковать историю полностью. Это, опять же, половина
команды из svn - svn commit
, а, точнее, та её часть, которая
отправляет изменения в удалённый репозиторий.
Для того же, чтоб создать новые ревизии, используется команда commit
.
В случае hg она создаёт их только локально, никак не оповещая об этом
удалённые репозитории. В качестве параметров принимает имена изменённых
файлов, которые следует внести в ревизию (при отсутствии параметров -
вносит все файлы).
update/revert
Так как вся информация о ревизиях у нас хранится локально, то прыгать по
ним можно быстро и безболезненно, для чего используется команда update
- вторая половина
update
из svn. Она «обновляет» (в кавычках, потому что можно «обновиться» и на ревизию старше текущей) рабочую копию до какой-то определённой ревизии (по умолчанию - до последней). Все незакоммиченные изменения будут слиты с новой ревизией рабочей копии (или проигнорированы, если указать опцию-C
). Отличие от варианта из svn заключается в том, что эта команда всегда работает только с локальными данными (не делая запросов к удалённому репозиторию).
revert
делает похожую операцию, но не переключая текущую ревизию
рабочей копии. Это, конечно, если не учитывать то, что revert
предназначен больше работать над индивидуальными файлами (хотя
поддерживает фильтрование и исключение файлов для работы над группами
файлов), а update
не имеет такой возможности. :)
Отмена ревизии
Отмена изменений, сделанных в какой-то ревизии - достаточно спорная
тема, учитывая философию неизменности истории. Но если всё же в
репозиторий попал коммит с, например, неправильным автором, под рукой
есть команда rollback
, которая откатывает последнюю транзакцию (список
команд есть в помощи по команде, commit
там присутствует). При этом
все локальные изменения, если они были, теряются (рабочая копия
изменяется до того состояния, которое было на момент коммита).
backout
- команда, подходящая к вопросу отмены ревизии с другой
стороны. Она создаёт ревизию, ответвляющуюся от исходной, в которой
отменены все изменения (или не все, указывается фильтрами, как в
revert
), после чего ревизия сливается с текущей веткой разработки.
Просмотр истории
Просмотр истории в меркуриале - вещь куда более весёлая, чем в svn, потому что происходит быстро и даёт в руки достаточное количество инструментов для фильтрации и настройке вывода.
Фильтровать можно многое - показывать (или скрывать) ревизии, содержащие
в себе изменения какого-то конкретного файла, показывать/скрывать
ревизии-слияния, ограничивать по дате, искать ключевое слово (в
авторе/описании ревизии)… log
также может показывать диффы этих
ревизий.
Ещё к теме о просмотре истории можно упомянуть cat
- вывод файла
определённой ревизии, можно в другой файл (поддерживает форматирование
имени), tip
- краткий аналог log -r tip
, id
- совсем-совсем
сокращённый (и по выводу тоже) аналог log -r .
Что мне нравится в меркуриаловском выводе лога по сравнению с таковым в svn - это возможность быстрого поиска и шаблоны.
С помощью log -k some-criteria
можно в считанные доли секунды увидеть
все ревизии, которые содержат этот критерий в описании или в имени
автора.
Вторая приятная штука - шаблоны вывода. Они позволяют настроить внешний
вид всех операций, которые выводят список ревизий. К примеру, я часто
использую sheads
- это мой алиас для heads
, который использует
шаблон для сокращения вывода, выглядит вот так:
Аналогично я использую, например, slog
вместо log
- намного короче и
проще быстро просмотреть, к чему дело идёт. :-) Мне кажется, что
синтаксис довольно прост и понятен с первого взгляда - в фигурных
скобках ключевое слово, через |
к нему - фильтры. Фильтры - это
небольшие функции на питоне, которые модифицируют данные - всё логично.
Фильтры, что естественно, можно строить в цепочки. Список всех ключевых
слов и фильтров можно увидеть в
помощи в hgweb.
Поиск по репозиторию
Бывают такие ситуации, когда поиска по описанию и имени автора явно
недостаточно, и надо найти какой-то кусочек кода, уже затерявшийся в
седых глубинах ревизий. В такие моменты на помощь приходит hg grep
-
grep
, который ищет не только в ширину (по разным файлам), но и в
глубину (по разным ревизиям).
Сказать о нём больше особо нечего - кто не знает, что такое grep? :-) Разве что упомянуть о том, что ограничивать поиск можно, как и многие другие операции, по ревизиям и по файлам.
Входящие/исходящие изменения
Вот этих двух конкретных команд - incoming
и outgoing
- мне часто не
хватает при работе с git’ом. Первая показывает список ревизий, которые
существуют в удалённом репозитории, но не существуют в локальном.
Последняя - показывает ревизии, которые существуют в локальном
репозитории, но не существуют в удалённом.
Удобно, если ещё неизвестно, стоит ли обновляться. :)
Игнорирование файлов
Список игнорируемых файлов находится не где-то в метаданных, а в обычном
версионируемом файле .hgignore
, который лежит в корне репозитория (не
в .hg/
, а рядом). Поддерживает переключение между двумя синтаксисами в
одном файле, один из которых - регулярные выражения, второй -
подстановки из шелла.
Теги
Теги в меркуриале реализованы в виде крайне простой сущности - это
обычный версионирующийся файл под названием .hgtags
в корне
репозитория, в котором хранятся пары “sha1-hash tag-name”.
hg tag tagname
создаст новый тег, -r revspec
позволяет явно указать
ревизию, которую надо затегать. Создание тега - это появление нового
коммита.
Теги могут быть локальные (создаются с помощью указания опции -l
),
тогда они не коммитят ничего в репозиторий, хранятся в файле
.hg/localtags
и между репозиториями не передаются.
Bundle
bundle
я уже упоминал раньше, когда рассказывал про обмен ревизиями по
email. Это такая упакованная (по умолчанию - с помощью bzip2) группа
ревизий, которая занимает очень мало места (интересный факт - несмотря
на то, что у git’а сам репозиторий занимает меньше места, чем у hg,
бандлы получаются большего размера). Очень мало -в прямом смысле слова,
при размере репозитория в 5.4 Мб (byteflow), полный бандл занимает 1.3
Мб.
Полезен для хранения «консервированных» репозиториев, а также для переноски на всяких флешках (записать один крупный файл намного проще, чем много мелких).
Работать с ним не просто, а очень просто - для hg бандл виден как удалённый репозиторий, из которого возможно только чтение. Т.е. работают те же команды, которые применяются при работе с удалёнными репозиториями, например:
Точно так же работают hg pull
и hg incoming
.
Недостатки
Вот мы и подобрались к самой спорной части статьи. :) Спорная она потому, что достаточно индивидуальная, и для разных людей разные особенности оборачиваются плюсами или минусами: иногда пытаются показать на такую проблему, как отсутствие встроенной утилиты для слияния. Для меня же это явное преимущество - ни под каким соусом мне не пытаются пропихнуть замену-недоделку для моего любимого Ediff, плюс усилия разработчиков не распыляются на создание утилиты, в общем-то связанной с самой системой контроля версий довольно посредственно.
Но всё-таки кое-какие недостатки я для себя вижу, как снаружи, так и внутри. Из внутренних проблем самая, наверное, яркая - это то, что переименование файла по сути дела копирует его, оставляя пометочку, что история его продолжается из какого-то предыдущего места дислокации. Для пользователя неудобствами не грозит, кроме случаев, когда этот файл занимает много мегабайт - переименование по сути дела увеличит репозиторий на размер этого файла. В большинстве случаев мелочь, но неприятная.
UPD: проблема решена. Из внешних же самый неприятный - скорее не недостаток, а отсутствие реализации. Было бы очень неплохо иметь для веток в графе репозитория имена - т.е. то, что в git’е называется named branch. На самом деле, есть уже кое-какая реализация этой штуки - hgbookmarks, но не завершённая - их ещё нельзя передавать по сети.
UPD: проблема решена, работающий rebase
появился давным-давно. Наверняка
со стороны приверженцев git’а можно услышать об отсутствии rebase (это уже явно
совсем не базовый уровень, но всё-таки :-)), но, во-первых, он
эмулируется
с помощью mq (о Mercurial Queues - в продолжении этой статьи), а во-вторых, есть
уже работающий
проект GSoC.
И совсем специфичная штука, которую мне в последние несколько дней довелось обсудить - возможность фальсификации изменений. Естественно, существуют пути обхода, целых два:
- пропускать на сервере только те ревизии, которые сделаны текущим пользователем
- использовать gpg для подписи изменений
Первое несколько урезает распределённость VCS (возможность обмениваться ревизиями с другими людьми улетучивается абсолютно), а вторая требует разбирательств, обучения людей и т.п. Но в принципе, такое может случиться только в очень специфическом окружении. :)
Summary
Несмотря на то, что последует ещё как минимум одно продолжение, мне кажется, что данной статьи уже достаточно, чтоб начать продуктивно работать с меркуриалом. Для тех, кому не терпится узнать больше прямо сейчас, есть более подробный документ, даже книга - hgbook. К сожалению, она ещё не дописана и тем более не переведена на русский, но уже достаточно хорошо наполнена информацией.
Кстати о продолжении - оно будет посвящено расширениям меркуриала. А точнее -тем из них, которые используются (практически) ежедневно: bisect, mercurial queues, graphlog, record, а также альтернативным (консоли) интерфейсам.
См. продолжение: Mercurial: расширения.