solovyov.net

Mercurial: основы

· · 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 плюса:

  • В репозиторий откровенно плохой код не попадает (так как тимлид хоть как-то, но просматривает этот код);
  • Обмен кода между разработчиками происходит быстро и просто, с помощью отправления писем или использования 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: расширения.