Грепал Грека грепом репо,
Видит Грека — в репо баг,
Сунул Грека в репо руку —
Баг за руку Греку цап!
Mercurial — распределённая система контроля версий, написанная на Python. Работает очень шустро (быстрее, похоже, только git), имеет всё что нужно и проста в работе. В Emacs для работы с Mercurial можно использовать VC, там есть нужный backend.
Решил тут поставить Mercurial на сервер, прицепить веб-интерфейс к нему и сделать так, чтобы можно было отправлять коммиты прямо на сервак через SSH и HTTPS (удобно и надёжно).
Опишу весь процесс.
При установке использовалось следующее ПО (с другими версиями что-то может измениться):
-
FreeBSD 6.2-STABLE в качестве OS на сервере
-
Веб-сервер lighttpd 1.4.15 (на версиях ниже 1.4.12 скорее всего будут проблемы с SSL; 1.4.x вообще не сахар по сравнению с грядущей 1.5.x, далее по тексту будет хак, связанный с недостатками ветки 1.4.x)
-
Mercurial 0.9.4 (вышла на днях)
Установка Mercurial дома и на сервере
Даже если ставить руками — тупо до безобразия, см. README.
На домашней машине (Gentoo Linux) хватило emerge mercurial.
На FreeBSD в портах есть devel/mercurial (пока только версии 0.9.3) —
хватит make install clean.
Правильность установки Mercurial проверяется вызовом команды
hg debuginstall.
Just in case you didn't know
lighttpd нужно перезагружать каждый раз после изменения его настроек в
файле lighttpd.conf.
Базовая настройка сервера
Для начала я создал под работу с Mercurial отдельного пользователя с
минимальными правами, репозитарии предполагалось хранить в его домашнем
каталоге (обычное решение — хранение репозиториев в /var/repos — имхо
не рулез, если разрешать SSH-доступ к репам).
В качестве домашнего каталога пользователь scm получил /home/scm/.
Дополнительно создал директорию /home/scm/hg/, в которой и будут
лежать репо. Права на этот каталог hg — 700.
Вместе с пользователем появилась и группа scm, в которую я
дополнительно поместил пользователя www, от имени которого у меня
запускается lighttpd — это потребуется для работы FastCGI-скрипта.
Теперь стоит проверить, можно ли работать с сервером из Mercurial через
прямой SSH-доступ. Заходим в любой Mercurial-репозиторий на домашней
машине, пусть это будет папка с именем libmerctest:
$ hg clone . ssh://scm@sphinx.net.ru/hg/libmerctest/
Должен появиться запрос пароля пользователя scm (или не должен, если доступ по ключу), после
чего на сервере в папке /home/scm/hg/ появится репозиторий
libmerctest. Последний будет пуст, потому что при клонировании (а так
же при hg push и hg pull) репо не обновляется рабочая копия
проекта. Путь после имени узла указывается относительно домашней
директории пользователя scm! Если случайно отзеркалить прямо в /hg/,
всё сломается :-)
Можно теперь попробовать обратную операцию: скачать залитый на сервер репозиторий на локальную машину:
$ hg clone ssh://scm@sphinx.net.ru/hg/libmerctest/
Внести изменения, закоммитить их (hg commit -m 'Test!') и отобразить
изменения на удалённый сервер:
$ hg push
На данном этапе все эти операции должны работать! Проблемы обычно
связаны с недостатком прав у пользователя scm.
Если при попытке сделать hg push в репозиторий через ssh появляется
ошибка Permission denied: .hg/store/lock (а ведь shit happens), нужно
проверить права на папки с репозиториями: пользователь scm должен
иметь к ним полный доступ на чтение/запись/обход (то есть, 700).
Если предполагается активное использование SSH-доступа к репам несколькими людьми, то есть смысл пользоваться рецептами по Shared SSH из Mercurial Wiki.
Можно также посадить юзера scm в jail или сделать ему chroot в
домашнюю директорию, чтобы по серверу не шарился. В данной статье это не
рассмотрено, RTFM.
Настройка веб-интерфейса к Mercurial через FastCGI
В дистрибутиве Mercurial 0.9.4 есть несколько CGI-скриптов на Python:
hgweb.cgi, hgwebdir.cgi и (появилось в последней версии) в папке
contrib — hgwebdir.fcgi. Первый предназначен для предоставления
через CGI веб-морды к одному репозиторию, второй — ко многим.
hgwebdir.fcgi — для работы через FastCGI. Это гораздо быстрее, чем
обычный CGI, так что я буду использовать именно hgwebdir.fcgi. Для
обычных .cgi-скриптов потребуются немного другие настройки (проще).
Я скопировал hgwebdir.fcgi в /home/scm/hg/, повесил на него права
500 (больше и не надо) и владельца — scm, а также создал для него
конфиг с именем hgweb.conf в той же папке с таким содержимым:
[collections] /hg = .
Что означает следующее: через веб-интерфейс будут видны все
Mercurial-репозитории в текущей папке (/home/scm/hg). Это удобно в том
смысле, что после настройки можно будет просто зеркалить локальные
репозитории на сервер, и они сразу станут видны через в веб-интерфейс.
Скрипт будет запускаться прямо из /home/scm/hg, причём FastCGI-процесс
будет инициироваться не веб-сервером, а отдельно. lighttpd и
hgwebdir.fcgi будут общаться через сокет.
Настройка lighttpd
Теперь нужно настроить lighttpd так, чтобы по адресу
http://sphinx.net.ru/hg/ появлялся список репозиториев и с ними можно
было нормально работать дальше.
Для этого в lighttpd.conf потребуется:
-
Подключить
mod_fastcgi, если ещё не подключен:server.modules += ("mod_fastcgi")
-
Переписать все запросы по адресу
http://sphinx.net.ru/hg/blahblahна/hg.fcgi/blahblah. По адресуhg.fcgiпозднее прикрепим FastCGI-сервер, который будет связываться через сокет сhgwebdir.fcgi. Кроме того, веб-интерфейс использует CSS-файлы, запрашиваемые по адресам/hg/static/:url.rewrite += ( "^(/hg/static.)$" => "$1", "^/hg(/?.)$" => "/hg.fcgi$1" )
(Я добавил странное перенаправление запросов по
/hg/staticна самих себя потому, что у меня далее по спискуurl.rewriteстоит переброс всех остальных запросов на уровень обработчика URL-ов Django:"^(/.*)$" => "/django.fcgi$1". Вероятно, на другом сервере такое перенаправление не понадобится.) -
Статические CSS-файлы устанавливаются вместе с модулями Mercurial в папку
templates, потому и для/hg/static/требуется подменаdocument-root:alias.url += ( "/hg/static/" => "/usr/local/lib/python2.5/site-packages/mercurial/templates/static/", )
-
По адресу
/hg.fcgiсделать FastCGI-сервер и прицепить его к сокету, который будет создаваться отдельно:fastcgi.server += ( "/hg.fcgi" => (( "socket" => "/var/tmp/hgwebdir.socket" )) )
В основном document-root lighttpd соответственно нужно создать пустой
файл hg.fcgi при помощи touch.
На данном этапе по lighttpd это всё.
FastCGI-процесс
Нужно создать FastCGI-процесс с сокетом в /var/tmp/hg.socket. Проще
всего написать rc-скрипт, который будет поднимать-опускать
hgwebdir.fcgi автоматически, внедрив его в систему загрузки сервера.
Можно прочитать руководство FreeBSD по написанию rc-скриптов, однако
вот готовый вариант, использующий утилиту spawn-fcgi из комплекта
поставки lighttpd:
#!/bin/sh # # REQUIRE: DAEMON # PROVIDE: hgwebdir # KEYWORD: shutdown #
. /etc/rc.subr
name="hgwebdir"
rcvar=set_rcvar
load_rc_config $name
pidfile=/var/run/${name}.pid procname="python" socket=/var/tmp/${name}.socket
command="/usr/local/bin/spawn-fcgi" command_args=" -f /home/scm/hg/hgwebdir.fcgi -s ${socket} -C /home/scm/hg -P ${pidfile} -u scm"
start_precmd=start_precmd start_postcmd=start_postcmd stop_postcmd=stop_postcmd
start_precmd() { cd ~scm/hg }
start_postcmd() { chown :scm ${socket} chmod 770 ${socket} }
stop_postcmd() { rm -f ${pidfile} ${socket} }
run_rc_command "$1"
Это нужно поместить в rc-скрипт по адресу
/usr/local/etc/rc.d/hgwebdir, а в /etc/rc.conf добавить строчку:
hgwebdir_enable="YES"
Тогда можно будет запускать/останавливать веб-интерфейс простыми командами:
/usr/local/etc/rc.d/hgwebdir start
и
/usr/local/etc/rc.d/hgwebdir stop
И при перезагрузке системы hgwebdir не отвалится, а загрузится сам.
При использовании spawn-fcgi созданный процесс будет иметь имя
python /home/scm/hg/hgwebdir.fcgi, что не совпадает с command, так
что инфраструктура rc из FreeBSD не сможет корректно отследить
запущенный процесс через check_pidfile(), в результате чего не будет
работать команда /usr/local/etc/rc.d/hgwebdir stop, ругаясь на якобы
незапущенный сервис. Чтобы этого не происходило, в скрипте прописана
строчка procname="python".
Возникают также проблемы с видимостью репозиториев. В hgweb.config
путь после = в строчке /hg = . указан относительно текущей рабочей
директории (по-другому просто не работает). Запустить
/usr/local/etc/rc.d/hgwebdir start можно откуда угодно, так что не
всегда hgwebdir.fcgi увидит репозитории в «текущей папке». Чтобы этого
не случалось, в скрипте явно прописан переход в папке с репозиториями в
start_precmd. Вероятно, это грязный хак, однако мне не удалось
заставить hgwebdir.fcgi воспринимать абсолютные пути. Можно также
делать переход в /home/scm/hg прямо в hgwebdir.fcgi (cм.
os.chdir() в Python). При работе через обычный CGI получается
подменять document-root для hgwebdir.cgi ещё на уровне lighttpd
через директиву alias.url.
Сокет помещается в /var/tmp: не думаю, что это распространённая
практика (и это снова грязный хак), но FastCGI-процесс спавнится от
имени пользователя scm (в command_args есть -u scm) и так
получается максимально урезать права веб-интерфейсу. Нужно лишь, чтобы
lighttpd имел полный доступ к сокету (несколькими частями ранее я
поместил в группу scm пользователя www) — это выполняется в
start_postcmd() rc-скрипта.
Дополнительно веб-интерфейс чрутится в /home/scm/hg/.
Проверка веб-интерфейса
На данном этапе можно запустить веб-интерфейс:
/usr/local/etc/rc.d/hgwebdir start
И указать браузеру на http://sphinx.net.ru/hg/, чтобы улицезреть
список своих репо.
Если на данном этапе появляется 502-я ошибка, то либо отвалился FastCGI-процесс, либо lighttpd не хватает прав на запись в сокет. В любом случае, сразу нужно смотреть в логи lighttpd.
Если не видятся репозитории — проверить права на них в папке
/home/scm/hg.
Помимо веб-интерфейса должно работать зеркалирование через HTTP с удалённого сервера:
$ hg clone http://sphinx.net.ru/hg/libfoobar
Настройка доступа в репозитории на запись через HTTPS
Mercurial позволяет делать hg push не только через SSH, но и через
HTTPS. Достоинство в том, что на уровне каждого репозитория можно
ограничивать набор пользователей, которые имеют доступ на запись к нему,
и не давать каждому SSH-доступ к серверу (пусть это даже доступ к
ограниченной учётке scm). Пользователи идентифицируются при помощи
стандартного механизма HTTP-аутентификации. Mercurial также
позволяет использовать SSL для защиты соединения.
Уровень Mercurial
В Mercurial нужно указать список пользователей, которым можно делать
hg push в репо. Например, в файле .hg/hgrc соответствующего
репозитория можно прописать
[web] allow_push = scm, bonobo
чтобы разрешить доступ на запись пользователям scm и bonobo. Указав
allow_push = *, мы разрешим любому пользователю, прошедшему
аутентификацию, толкать в репо (ok-ok, делать hg push).
По умолчанию Mercurial поддерживает hg push только через HTTPS, чтобы
разрешить запись через HTTP нужно в секции [web] файла .hg/hgrc
соответствующего репозитория прописать:
push_ssl = false
Но я буду рассматривать случай записи в репо с использованием только HTTPS.
Настройка lighttpd
Теперь нужно настроить доступ по HTTPS на lighttpd и сделать так, чтобы при доступе в репозиторий появлился запрос аутентификации ползователя.
С первым всё просто. Нужно, чтобы lighttpd был скомпилен с поддержкой SSL:
$ lighttpd -v lighttpd-1.4.15 (ssl) - a light and fast webserver
Для работы потребуется SSL-сертификат. Если его нет, то можно сгенерить самому:
$ openssl req -new -x509
-keyout lighttpd.pem -out lighttpd.pem
-days 365 -nodes
OpenSSL задаст несколько вопросов, в «Common Name:» лучше указать домен.
Полученный файлик lighttpd.pem поместить в /etc/ssl/certs/. Браузеры
на такой самопальный сертификат будут ругаться, потому что он не
подписан удостоверяющим центром.
Теперь в lighttpd.conf подключить используемый сертификат (для
использования при HTTPS-соединениях):
$SERVER["socket"] == ":443" { ssl.engine = "enable" ssl.pemfile = "/etc/ssl/certs/lighttpd.pem" }
Если есть корневой сертификат от CA, то путь к нему нужно указать там
же, в ssl.ca-file. В доках по lighttpd есть подробности.
По-хорошему, лучше разрешить доступ на чтение без пароля, а требовать авторизации только при попытке записи в репозиторий. На уровне HTTPS это ловится типом запроса — POST или GET. Но в версиях lighttpd 1.4.x нет возможности указывать настройку только для определённого типа запроса (эта возможность добавлена в ветке 1.5.x). Можно обойтись так: сделать на уровне аутентификационного механизма lighttpd дополнительного пользователя, которому на уровне Mercurial всё равно будет запрещён доступ на запись в репозитории. Далее поясню это подронее.
В lighttpd.conf включаем механизм авторизации:
server.modules += ( "mod_auth" )
Указываем там же используемый тип хранения учётных записей:
auth.debug = 0 auth.backend = "plain" auth.backend.plain.userfile = "/usr/local/etc/lighttpd.auth.plain"
При типе plain пароли хранятся в файле открытым способом и
перечисляются в парах «имя-пароль» через двоеточие:
$ cat /usr/local/etc/lighttpd.auth.plain hgview:hgview scm:$up3rb!ff_(C)
Для типа htpasswd файл с шифрованными паролями указывается в
переменной auth.backend.htpasswd.file; его можно создать при помощи
одноимённой программы htpasswd из Apache:
$ cd /usr/local/etc/ $ htpasswd -bc lighttpd.auth.htpasswd hgview hgview $ htpasswd -b lighttpd.auth.htpasswd scm '$up3rb!ff_(C)' $ cat lighttpd.auth.htpasswd hgview:mxgU6lcV.0CPM scm:ZQmywuFRcxzPq
Тип htdigest предоставляет возможность ещё более надёжного
хранения паролей.
И вешаем на все запросы по /hg (_без_ слеша на конце!) волшебное
окошко с паролем:
auth.require = ( "/hg" => ( "method" => "basic", "realm" => "Mercurial repos (hgview/hgview for readonly access)", "require" => "valid-user" ) )
Строчка "require" => "valid-user" означает, что авторизацию пройдёт
всякий, указавший правильное сочетания имени и пасса из файла
/usr/local/etc/lighttpd.auth.plain.
В "realm" => ... указывается сообщение в окошке ввода пароля.
«Фиктивный» пользователь hgview был создан для простого доступа на
чтение. Можно всю конструкцию auth.require обернуть в условие
$SERVER["socket"] == ":443" { .. }, тогда при доступе по http://
пароль спрашиваться не будет (всё равно hg push я разрешил только по
https://).
Когда lighttpd 1.5.x уверенно станет стабильным
(http://lighttpd.net сам уже работает на этой версии), можно
будет auth.require завернуть в условие
$SERVER["request-method"] == "POST" { .. } и убрать пользователя
hgview вообще — тогда запрос на авторизацию будет появляться только
при попытке записи в репо.
Пользователь scm же включен в списки allow_push в настройках
репозиториев (в файлах .hg/hgrc) и может после авторизации делать
hg push:
$ cd ~/projects/work-libfoobar/ $ hg push https://scm@sphinx.net.ru/hg/libfoobar
Появится запрос пароля и изменения улетят на сервер.
Пароль при доступе через https://scm@sphinx.net.ru/... — тот, что
указан в lighttpd.auth.plain! Это пароль уровня lighttpd, а не пароль
пользователя scm на сервере. Я просто сделал им одинаковые имена
(можно и пароли одинаковые сделать), чтобы не путаться при доступе через
SSH и HTTPS.
Если Mercurial вылетает с ошибкой abort: authorization failed (shit
happens) при стопудово правильном пароле, стоит проверить разрешения на
запись в репозитории в файлах .hg/hgrc.
Добавление коммиттера (HTTPS)
Когда потребуется разрешить стороннему пользователю запись в какой-то репозиторий через HTTPS, нужно будет:
-
Создать новую пару имя-пароль в файле
lighttpd.auth.plain(или другом, в зависимости от используемого типа хранения учёток). -
Внести созданное имя в список
allow_push =в настройках соответствующего репозитория Mercurial (напрямую через SSH).
Перезагрузка сервисов не потребуется.
Через HTTPS в Mercurial нельзя делать hg clone.
Индивидуальные настройки репозиториев
Каждому репо можно указать описание и человеческое название в
.hg/hgrc:
[web] name = libfoobar description = Blazingly fast library for intensive foobaring
Крайне полезна опция allow_archive, позволяющая скачивать архивы со
снимками ревизий через веб интерфейс:
allow_archive = gz bz2
Дальнейшая информация
В Mercurial есть и другие вкусности, обязательно читать Mercurial wiki.
К слову, шаблоны веб-интерфейса по умолчанию — антикошерные.