solovyov.net

Mercurial: основы

14 min read · hg, vcs

Продолжение, см. начало: 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 плюса:

Могу сказать с уверенностью, что эта схема действительно является заметно более удачной, чем традиционная работа с svn.

Особенности работы

Несмотря на то, что базовые команды действительно похожи на набор команд из svn, разница всё равно существенна. Поэтому далее я вкратце опишу те операции, которыми приходится пользоваться достаточно часто, без подробного расписывания каждой опции - это можно прочесть в помощи, используя hg help command.

Как указывать ревизии

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

Идентификатором ревизии может служить либо её порядковый номер (который не является чем-то стабильным и легко может поменяться при, например, отправлении своих ревизий в другой репозиторий), либо 16-ричный хеш sha1 длиной в 40 цифр, либо тег (tag, описаны ниже). Также существует три зарезервированных имени:

Указание группы ревизий имеет синтаксис, аналогичный синтаксису 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

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.

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

Первое несколько урезает распределённость VCS (возможность обмениваться ревизиями с другими людьми улетучивается абсолютно), а вторая требует разбирательств, обучения людей и т.п. Но в принципе, такое может случиться только в очень специфическом окружении. :)

Summary

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

Кстати о продолжении - оно будет посвящено расширениям меркуриала. А точнее -тем из них, которые используются (практически) ежедневно: bisect, mercurial queues, graphlog, record, а также альтернативным (консоли) интерфейсам.

См. продолжение: Mercurial: расширения.

If you like what you read — subscribe to my Twitter, I always post links to new posts there. Or, in case you're an old school person longing for an ancient technology, put a link to my RSS feed in your feed reader (it's actually Atom feed, but who cares).

Other recent posts

History snapshotting in TwinSpark.js
Code streaming: hundred ounces of nuances
Useful shell prompt
API pagination design
ElasticSearch query builder