Mercurial: основы
Продолжение, см. начало: Mercurial: введение в распределённые системы контроля версий
Одним из несомненных плюсов меркуриала является его интерфейс, который покажется знакомым каждому, кто хоть какое-то время работал с subversion в командной строке. Естественно, что отличий хватает (сам принцип работы совершенно иной), но наиболее часто употребляемые команды — очень похожи. Рассматривать я буду только командную строку — во-первых, я ею пользуюсь сам, во-вторых, ни один GUI не подходит и близко по возможностям, и, в-третьих, она значительно ближе «к телу» — проще понять, что, как и почему так работает.
Базовые принципы
Сразу в двух словах опишу модель данных в меркуриале (и большинстве других DVCS) — она несколько отличается от привычной по централизованным системам.
Ревизия (changeset) — это сущность, описывающая изменения в каких-либо файлах. Эта сущность хранит информацию о своём авторе, времени создания, изменениях в файлах (естественно ;) и о родительских ревизиях (которых может быть одна в случае обычной ревизии и две в случае слияния). Кстати, подсчёт
Эти ревизии (благодаря наличию у себя родителей) организовывают ориентированный ациклический граф (от ревизии 0 и до существующих голов), который может разветвиться в любом месте.
Головы — это ревизии, у которых (ещё) нет детей, конечные точки графа.
При этом существуют две специальные ревизии — null (ревизия-родитель самой первой ревизии под номером 0) и tip (самая последняя ревизия, в случае наличия нескольких голов выбирается в зависимости от обстоятельств).
Основы работы
Скачать hg для Windows (под никсами, я думаю, каждый и так знает, как пользоваться менеджером софта) можно здесь, в довесок к этому дистрибутиву идёт GUI, которым вроде бы даже можно пользоваться. :) Сразу упомяну, чтобы не забыть, о необходимости настройки меркуриала. Для этого надо отредактировать конфигурационный файл, который находится в ~/.hgrc
в случае *nix, и в %USERPROFILE%\mercurial.ini
в случае Windows. Надо туда добавить пару строчек:
[ui]
username = Alexander Solovyov <piranha@gtv>
Я думаю, они говорят сами за себя. :) hgrc
в настоящий момент — самый обычный ini-файл, знакомый, я думаю, каждому. :)
Всё начинается с того, что мы создаём репозиторий:
hg init repo
Если посмотреть внутрь директории repo
, можно увидеть директорию .hg
— это и есть сам репозиторий, в котором хранятся ревизии и настройки данного конкретного экземпляра (репозитория, простите за каламбур ;). Начать можно с добавления файлов и коммита первой ревизии:
cd repo
echo "something" > read.me
hg add
hg ci -m "Initial revision"
Теперь можно просмотреть историю репозитория с помощью hg log
:
changeset: 0:2add2e250fd2
tag: tip
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:36:39 2008 +0300
summary: Initial revision
Или склонировать его:
cd ..
hg clone repo repoclone
repoclone
— абсолютно идентичен repo
в плане наполнения, однако в нём уже появился файл .hg/hgrc
, в котором записано, что путь по умолчанию (для забирания и отдавания изменений) — это предыдущий repo
:
[paths]
default = /home/piranha/repo
Теперь, если мы сделаем изменения в обоих репозиториях, мы можем увидеть такую картину:
piranha@gtv ~/repoclone>hg incoming
comparing with /home/piranha/repo
searching for changes
changeset: 1:99a5a4fc7247
tag: tip
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:40:51 2008 +0300
summary: Another commit
piranha@gtv ~/repoclone>hg outgoing
comparing with /home/piranha/repo
searching for changes
changeset: 1:7cb00df3cf09
tag: tip
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:41:21 2008 +0300
summary: commit in clone
Команды incoming
и outgoing
показывают соответственно входящие (те, которых ещё нету в локальном репозитории) и исходящие (те, которых не хватает в удалённом репозитории) ревизии. Обратите внимание, что я не указывал путь к удалённому репозиторию — меркуриал в этом случае берёт его из пути по умолчанию (который можно увидеть раньше). Или ругается, в случае его отсутствия. :)
Merge
Сложившаяся ситуация подразумевает слияние — у нас появились две ветки разработки, значит пора применять merge
:
piranha@gtv ~/repoclone>hg pull
pulling from /home/piranha/repo
...
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)
piranha@gtv ~/repoclone>hg heads
changeset: 2:99a5a4fc7247
tag: tip
parent: 0:2add2e250fd2
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:40:51 2008 +0300
summary: Another commit
changeset: 1:7cb00df3cf09
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 14:41:21 2008 +0300
summary: commit in clone
piranha@gtv ~/repoclone>hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
piranha@gtv ~/repoclone>hg ci -m "merge"
piranha@gtv ~/repoclone>hg tip
changeset: 3:a81fdfac4695
tag: tip
parent: 1:7cb00df3cf09
parent: 2:99a5a4fc7247
user: Alexander Solovyov <piranha@gtv>
date: Tue Jul 22 17:26:51 2008 +0300
summary: merge
Что произошло? Мы вытянули (pull
) отсутствовавшую ревизию из удалённого репозитория (получив при этом две ветки локально), посмотрели на головы (heads
— разъяснено ниже), слили изменения и зафиксировали это коммитом (ci
— алиас для commit
).
Некоторые пояснения: heads
— команда, которая демонстрирует наличие двух (т.е. двух разошедшихся веток разработки). merge
сливает ветки и говорит о том, что относительно первого родителя у нас обновился 1 файл. Ну и tip
показывает, что у получившейся ревизии есть два родителя. Итого картина репозитория оказывается вот такой:
piranha@gtv ~/repoclone>hg glog
@ changeset: 3:bd58c96d6690
|\ tag: tip
| | parent: 2:99a5a4fc7247
| | parent: 1:7cb00df3cf09
| | user: Alexander Solovyov <[email protected]>
| | date: Wed Aug 06 14:21:40 2008 +0300
| | summary: merge
| |
| o changeset: 2:99a5a4fc7247
| | parent: 0:2add2e250fd2
| | user: Alexander Solovyov <[email protected]>
| | date: Tue Jul 22 14:40:51 2008 +0300
| | summary: Another commit
| |
o | changeset: 1:7cb00df3cf09
|/ user: Alexander Solovyov <[email protected]>
| date: Tue Jul 22 14:41:21 2008 +0300
| summary: commit in clone
|
o changeset: 0:2add2e250fd2
user: Alexander Solovyov <[email protected]>
date: Tue Jul 22 14:36:39 2008 +0300
summary: Initial revision
Такие вот деревья hg рисует с помощью расширения graphlog. Описание его будет в продолжении этой статьи. Естественно, есть и графические аналоги для отображения графа ревизий.
Есть один момент, на который хотелось бы обратить внимание — у ревизии может быть максимум 2 родителя. Т.е. если у нас оказывается 3 головы, одним мержем их слить не получится — меркуриал обратит внимание на то, что голов много и неплохо бы явно указать, с какой из ревизий хочется слить текущую: hg merge -r rev
.
Обмен данными по сети
Один из наиболее приятных моментов hg для меня — это использование абсолютно стандартных протоколов для обмена данными между репозиториями: http (и https), ssh или, на худой конец, аттачами в email.
При этом никаких усилий или отдельных демонов (я предполагаю, что если хочется использовать http — какой-никакой веб-сервер уже работает :)) не требуется. ssh работает вообще без всякой настройки, а http-часть идёт в комплекте и требует веб-сервера, могущего cgi/fastcgi/wsgi (на выбор). Протоколы работы по ним очень похожи — локальный и удалённый меркуриалы обмениваются bundle’ами (далее по тексту — бандл), сжатыми файлами с группой ревизий (должен заметить, что эффективность довольно неплоха — бандл полного репозитория примерно в 2 раза меньше, чем его .tar.bz2
архив).
Интересный момент: по ssh меркуриал может не только вытягивать и заталкивать изменения, но и клонировать в удалённый хост:
hg clone repo ssh://piranha.org.ua/repo
Бывает полезно, когда сидишь за NAT’ом, да и вообще лень делать много лишних движений. :) Приятно, когда на той стороне оказывается уже настроенный hgweb и этот свежесклонированный репозиторий сразу оказывается опубликованным по http. :)
Правда, возможность работать по голому http/sftp (без меркуриала на удалённой стороне, как со статическими файлами) отсутствует, но как показывает практика соседних DVCS (и история самого hg), скорость такой работы настолько низка, что лучше даже и не пробовать.
Обмен изменениями по email в текущей инкарнации предлагает выбор из двух вариантов — мы либо шлём текстовый патч, либо бандл (описание — ниже по тексту) с нужными ревизиями. Первое позволяет просмотреть себя глазами, а второе гарантирует то, что применённые ревизии будут точно такими же, как отправленные. Однако в MyDeco была сделана версия команды отсылания изменений, которая слала одновременно и патч, и бандл — для удобства. :)
Рабочий процесс
Организация рабочего процесса — это, наверное, одна из самых замечательных особенностей DVCS. Распределённая система значительно меньше ограничивает полёт фантазии разработчиков, позволяя строить процесс в соответствии с их вкусом.
Я всё-таки не буду рассказывать обо всех возможностях, что может затянуться очень на долго, а просто вкратце расскажу, как построена работа в MyDeco (собственно, в byteflow она построена абсолютно аналогично, не считая меньшего количества разработчиков и меньшей формализованности).
На сервере существует так называемый главный репозиторий, ничем не отличающийся от других — его просто решили считать таковым. У каждого разработчика там же, на сервере, есть собственная папка, где он может создавать (и удалять) бесконечное количество клонов репозиториев (если вдруг возникнет потребность в таковых).
В главный репозиторий может отправлять изменения ограниченное количество людей — тимлиды (если их так можно назвать, все поделены на небольшие команды по
При этом любой разработчик может с помощью .htaccess
(использовался Apache) у себя в домашней директории (конечно, точнее говоря — в ~/public_html/
) может настроить свою собственную копию сайта, работающую на основе любого из своих репозиториев.
У подобной схемы организации работы существует как минимум 3 плюса:
- В репозиторий откровенно плохой код не попадает (так как тимлид хоть как-то, но просматривает этот код);
- Обмен кода между разработчиками происходит быстро и просто, с помощью отправления писем или использования
hg serve
(встроенный http-сервер), когда они находятся рядом; - Незаконченные, но требующие какого-то согласования возможности или баги можно легко продемонстрировать на своей собственной копии сайта, что помогает поддерживать репозиторий в более чистом виде.
Могу сказать с уверенностью, что эта схема действительно является заметно более удачной, чем традиционная работа с svn
.
Особенности работы
Несмотря на то, что базовые команды действительно похожи на набор команд из svn
, разница всё равно существенна. Поэтому далее я вкратце опишу те операции, которыми приходится пользоваться достаточно часто, без подробного расписывания каждой опции, потому что это можно легко увидеть, используя hg help command
.
Как указывать ревизии
Большинство команд (все, где это имеет смысл) имеют опцию -r
, с помощью которой можно указать, над какой конкретно ревизией (или группой ревизий) мы хотим произвести действие (логично, что по умолчанию — текущая).
Идентификатором ревизии может служить либо её порядковый номер (который не является чем-то стабильным и легко может поменяться при, например, отправлении своих ревизий в другой репозиторий), либо
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
, который использует шаблон для сокращения вывода, выглядит вот так:
sheads = heads --template '{date|shortdate} [{rev}:{node|short}] {author|person}: {desc|firstline}\n'
Аналогично я использую, например, slog
вместо log
— намного короче и проще быстро просмотреть, к чему дело идёт. :-) Мне кажется, что синтаксис довольно прост и понятен с первого взгляда — в фигурных скобках ключевое слово, через |
к нему — фильтры. Фильтры — это небольшие функции на питоне, которые модифицируют данные — всё логично. Фильтры, что естественно, можно строить в цепочки. Список всех ключевых слов можно увидеть в hgbook, список фильтров — там же, ниже по тексту.
Поиск по репозиторию
Бывают такие ситуации, когда поиска по описанию и имени автора явно недостаточно, и надо найти какой-то кусочек кода, уже затерявшийся в седых глубинах ревизий. В такие моменты на помощь приходит 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 бандл виден как удалённый репозиторий, из которого возможно только чтение. Т.е. работают те же команды, которые применяются при работе с удалёнными репозиториями, например:
piranha@gtv ~>hg clone byteflow.bun byteflow
requesting all changes
adding changesets
adding manifests
adding file changes
added 906 changesets with 2554 changes to 804 files
updating working directory
478 files updated, 0 files merged, 0 files removed, 0 files unresolved
Точно так же работают hg pull
и hg incoming
.
Недостатки
Вот мы и подобрались к самой спорной части статьи. :) Спорная она потому, что достаточно индивидуальная, и для разных людей разные особенности оборачиваются плюсами или минусами: вот товарищ bialix часто пытается показать на такую проблему, как отсутствие встроенной утилиты для слияния. Для меня же это явное преимущество — ни под каким соусом мне не пытаются пропихнуть замену-недоделку для моего любимого Ediff, плюс усилия разработчиков не распыляются на создание утилиты, в общем-то связанной довольно посредственно с самой системой контроля версий.
Но всё-таки кое-какие недостатки я для себя вижу, как снаружи, так и внутри. Из внутренних проблем самая, наверное, яркая — это то, что переименование файла по сути дела копирует его, оставляя пометочку, что история его продолжается из какого-то предыдущего места дислокации. Для пользователя неудобствами не грозит, кроме случаев, когда этот файл занимает много мегабайт — переименование по сути дела увеличит репозиторий на размер этого файла. В большинстве случаев мелочь, но неприятная.
Из внешних же самый неприятный — скорее не недостаток, а отсутствие реализации. Было бы очень неплохо иметь для веток в графе репозитория имена — т.е. то, что в git’е называется named branch. На самом деле, есть уже кое-какая реализация этой штуки — hgbookmarks, но не завершённая — их ещё нельзя передавать по сети.
Наверняка со стороны приверженцев git’а можно услышать об отсутствии rebase (это уже явно совсем не базовый уровень, но всё-таки :-)), но, во-первых, он эмулируется с помощью mq (о Mercurial Queues — в продолжении этой статьи), а во-вторых, есть уже работающий проект GSoC.
И совсем специфичная штука, которую мне в последние несколько дней довелось обсудить — возможность фальсификации изменений. Естественно, существуют пути обхода, целых два:
- пропускать на сервере только те ревизии, которые сделаны текущим пользователем
- использовать gpg для подписи изменений
Первое несколько урезает распределённость VCS (возможность обмениваться ревизиями с другими людьми улетучивается абсолютно), а вторая требует разбирательств, обучения людей и т.п. Но в принципе, такое может случиться только в очень специфическом окружении. :)
Summary
Несмотря на то, что последует ещё как минимум одно продолжение, мне кажется, что данной статьи уже достаточно, чтоб начать продуктивно работать с меркуриалом. Для тех, кому не терпится узнать больше прямо сейчас, есть более подробный документ, даже книга — hgbook. К сожалению, она ещё не дописана и тем более не переведена на русский, но уже достаточно хорошо наполнена информацией.
Кстати о продолжении — оно будет посвящено расширениям меркуриала. А точнее — тем из них, которые используются (практически) ежедневно: bisect, mercurial queues, graphlog, record, а также альтернативным (консоли) интерфейсам.
См. продолжение: Mercurial: расширения.
Все про українське ІТ в телеграмі — підписуйтеся на канал DOU
32 коментарі
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.