Собственный git-сервер с нуля
gitosis
Обновленная статья доступна на английском. Возможно, позже я приведу русский вариант в соответствие с ней.
Если вы активно программируете, то наверняка пользуетесь какой-либо системой контроля версий. Возможно, вы уже задумывались о том, чтобы перенести все свои репозитории с локальной машины/github/gitorious; на выделенный сервер или VPS. Причины могут быть разные: не доверяете публичным серверам, нужна серьезная площадка для проектов или просто хочется сделать дополнительное зеркало для надежности.
В любом случае, будем считать, что у вас уже есть свой Linux-сервер, и дело за малым — настроить его. Я сам недавно столкнулся с этой задачей, и в процессе ее решения я делал для себя записи, чтобы не забыть последовательность действий. А когда все настроил — подумал, что кому-то мой опыт может оказаться полезным. Для новичков в nix-мире (вроде меня), чем больше разнообразных HowTo, тем проще подобрать что-то под свою задачу; так что, хотя на хабре и на других сайтах есть схожие мануалы, моя статья все-таки копипастой не является, а значит — пусть будет.
Стенд
Все нижеприведенные манипуляции производились на машине с Ubuntu 10.04. Но, думаю, знающие люди смогут перенести эти инструкции на другие системы. Кое-где в статье будет упоминаться клиентская машина; будем считать, что на ней тоже запущено что-то nix-подобное.
Так как я лично ориентировался на VPS, для сервера была выбрана связка из быстрого веб-сервера nginx; и не менее быстрой веб-морды сgit;. Хотя, наверное, я бы сделал тот же выбор, будь у меня в распоряжении выделенный сервер — я испытываю иррациональную неприязнь к apache и gitweb.
В качестве имени сервера я буду использовать git.example.com. Тем не менее, при желании достаточно легко изменить инструкции так, чтобы держать все не в отдельном домене, а в поддиректории основного.
Фундамент: gitosis
Все репозитории на сервере будут управляться при помощи gitosis. Устанавливаем:
$ sudo aptitude install git-core gitosis
Создаем юзера для работы с репозиториями:
$ sudo adduser --system --shell /bin/sh --gecos 'git version control' --group --disabled-password --home /home/git git
Теперь нужно проинициализировать gitosis своим публичным ключом. Если его у вас еще нет, создайте его на клиентской машине при помощи ssh-keygen, затем скопируйте на сервер и скормите gitosis'у:
client$ scp ~/.ssh/id_rsa.pub user@git.example.com:/home/user $ sudo -H -u git gitosis-init < /home/user/id_rsa.pub
Если у post-update хука не стоит execution bit, то надо его проставить:
$ sudo chmod +x /home/git/repositories/gitosis-admin.git/hooks/post-update
Репозитории gitosis хранит в поддиректории repositories домашней папки юзера git. Все настройки и ключи хранятся в отдельном репозитории gitosis-admin. Вы можете работать с ним напрямую на сервере или же создать локальную копию на клиентской машине:
client$ git clone git@git.example.com:gitosis-admin.git
На данный момент в репозитории лежит файл настроек gitosis.conf и ключ, заданный при установке, в поддиректории keydir. Имя ключа — это имя пользователя, который этим ключом будет пользоваться. Так что если вам не нравится длинное имя, которое ему назначил gitosis, его можно сменить — но осторожно, если вы работаете с клиентской машины, потому что, если вдруг имя вашего ключа не будет совпадать с именем в конфиге, gitosis не позволит вам сделать push. Так что имя ключа в keydir и значение members в группе [gitosis-admin] файла gitosis.conf надо менять в одном коммите. В качестве примера для настройки gitosis создадим два репозитория: публичный и скрытый. Названия — задел на будущее: для gitosis категории публичности не существует, он лишь оперирует списком допущенных к редактированию людей. Итак, пусть ваш юзер зовется user (и, соответственно, ключ с вашего клиента лежит в keydir/user.pub). Тогда gitosis.conf выглядит следующим образом:
[gitosis] [group gitosis-admin] writable = gitosis-admin members = user [group myrepos] writable = publicrepo privaterepo members = user
Не забываем закоммитить изменения в файле:
client$ git commit -a -m "Create test repos" client$ git push
Создаем репозитории на клиентской машине:
client$ cd gitrepos client$ mkdir publicrepo client$ cd publicrepo client$ git init client$ git remote add mysrv git@git.example.com:publicrepo.git client$ touch test.txt client$ echo "first commit" > test.txt client$ git add test.txt client$ git commit -a -m "First commit" client$ git push mysrv master
То же самое проделываем для privaterepo.
Публичный доcтуп к репозиториям: git-daemon
Доступ с авторизацией настроен, теперь нужно настроить и git:// доступ только для чтения. Естественно применить для этого git-daemon, который уже установился вместе с git-core. Теоретически, для его запуска существует отдельный пакет git-daemon-run, но у него есть один, с моей точки зрения, серьезный недостаток: он использует не service, а sv. Такой непорядок я не терплю, поэтому предпочитаю создать init.d скрипт вручную.
Создайте файл /etc/init.d/git-daemon со следующим содержимым:
#! /bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin NAME=git-daemon PIDFILE=/var/run/$NAME.pid DESC="git daemon" DAEMON=/usr/lib/git-core/git-daemon DAEMON_OPTS="--base-path=/home/git/repositories/ --syslog --detach --pid-file=$PIDFILE --user=git --group=git" test -x $DAEMON || exit 0 [ -r /etc/default/git-daemon ] && . /etc/default/git-daemon . /lib/lsb/init-functions start_git() { start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_OPTS || true } stop_git() { start-stop-daemon --stop --quiet --pidfile $PIDFILE --retry 5 --exec $DAEMON || true rm -f $PIDFILE } status_git() { status_of_proc -p $PIDFILE "$DAEMON" $NAME && exit 0 || exit $? } case "$1" in start) log_begin_msg "Starting $DESC" start_git log_end_msg 0 ;; stop) log_begin_msg "Stopping $DESC" stop_git log_end_msg 0 ;; status) status_git ;; restart|force-reload) log_begin_msg "Restarting $DESC: " stop_git sleep 1 start_git log_end_msg 0 ;; *) echo "Usage: $NAME {start|stop|restart|force-reload|status}" >&2 exit 1 ;; esac exit 0
Не забываем разрешить его выполнение:
$ sudo chmod +x /etc/init.d/git-daemon
и добавить в автозагрузку, например, при помощи sysv-rc-conf (проставьте runlevels с 3 по 5). Теперь осталось только пометить все публичные репозитории при помощи специального файла:
$ sudo -H -u git touch /home/git/repositories/publicrepo.git/git-daemon-export-ok
После запуска сервиса вы сможете клонировать помеченные репозитории через git протокол.
Веб-интерфейс: nginx и cgit
Подготовительные меры
Первым делом, конечно, надо установить собственно веб-сервер:
$ sudo aptitude install nginx
Небольшая проблема с cgit состоит в том, что он поддерживает только CGI, а nginx поддерживает только FastCGI. Иногда для обхода этого ограничения используется второй веб-сервер специально для cgit, а в nginx прописывается перенаправление запросов. Мне же больше нравится подход с FCGI-оберткой для CGI: spawn-fcgi и fcgiwrap. Последняя утилита, к сожалению, отсутствует в репозитории, поэтому ее придется компилировать на месте. Желающие могут сделать из нее пакет сами, я же просто приведу простейший вариант установки. Итак:
$ sudo aptitude spawn-fcgi libfcgi-dev $ git clone git://github.com/gnosek/fcgiwrap.git $ cd fcgiwrap $ autoreconf -i $ ./configure $ make $ sudo make install
Создаем скрипт /etc/init.d/spawn-fcgi для сервиса spawn-fcgi, аналогично тому, как мы делали это для git-daemon:
#! /bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin NAME=spawn-fcgi PIDFILE=/var/run/$NAME.pid DESC="spawn-fcgi daemon" DAEMON=/usr/bin/spawn-fcgi DAEMON_OPTS="-f /usr/local/sbin/fcgiwrap -s /var/run/spawn-fcgi -u www-data -g www-data -P $PIDFILE" test -x $DAEMON || exit 0 set -e . /lib/lsb/init-functions start_spawn_fcgi() { start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_OPTS || true } stop_spawn_fcgi() { start-stop-daemon --stop --quiet --pidfile $PIDFILE --retry 5 || true rm -f $PIDFILE } status_spawn_fcgi() { status_of_proc -p $PIDFILE "$DAEMON" $NAME && exit 0 || exit $? } case "$1" in start) log_begin_msg "Starting $DESC: " start_spawn_fcgi log_end_msg 0 ;; stop) log_begin_msg "Stopping $DESC: " stop_spawn_fcgi log_end_msg 0 ;; status) status_spawn_fcgi ;; restart|force-reload) log_begin_msg "Restarting $DESC: " stop_spawn_fcgi sleep 1 start_spawn_fcgi log_end_msg 0 ;; *) echo "Usage: $NAME {start|stop|restart|force-reload|status}" >&2 exit 1 ;; esac exit 0
Так же, как и в случае git-daemon, скрипту надо проставить execution bit и добавить его в автозагрузку для runlevel-ов с 3 по 5.
Установка cgit
Cgit тоже придется устанавливать из исходников. Если вы клонируете его репозиторий, не забудьте выбрать нужную версию перед компиляцией (если вы не знаете, какая версия вам нужна, то выбирайте последнюю стабильную). Кроме того, стоит также выбрать версию исходников git соответствующую той, которая у вас установлена.
$ git clone git://hjemli.net/pub/git/cgit $ cd cgit $ git submodule init $ git submodule update $ git checkout v0.8.3.3 $ cd git $ git checkout v1.7.0.4 $ cd .. $ sudo aptitude install libcurl4-openssl-dev build-essential $ make
При компиляции будет создан исполняемый файл cgit. Его, а также файл стилей и логотип cgit надо скопировать в предназначенную для них директорию.
$ sudo mkdir /var/www/cgit $ sudo mkdir /var/www/cgit/static $ sudo mkdir /var/www/cgit/cgi-bin $ sudo cp cgit /var/www/cgit/cgi-bin/ $ sudo cp cgit.png /var/www/cgit/static/ $ sudo cp cgit.css /var/www/cgit/static/ $ sudo chown -R www-data:www-data /var/www/cgit
Cgit'у нужен конфиг, шаблон для которого с комментариями ко всем опциям лежит в директории компиляции (cgitrc.5.txt). Примерный конфиг выглядит следующим образом:
/etc/cgitrc:
virtual-root=/ # enable caching of up to 1000 output entries cache-size=1000 # page title for the root page (repo listing) root-title=Insert title here # description for the root page root-desc=Insert description here # link to css file css=/cgit.css # link to logo file logo=/cgit.png # Enable statistics per week, month and quarter max-stats=quarter # Specify some default clone prefixes clone-prefix=git://git.example.com ssh://git@git.example.com http://git.example.com/http # Show extra links for each repository on the index page enable-index-links=1 # Show number of affected files per commit on the log pages enable-log-filecount=1 # Show number of added/removed lines per commit on the log pages enable-log-linecount=1 # Caching disabled. We will use nginx's caching mechanisms # time-to-live settings: specify how long (in minutes) different pages should # be cached. specify 0 for instant expiration and -1 for immortal pages # ttl for root page (repo listing) cache-root-ttl=0 # ttl for repo summary page cache-repo-ttl=0 # ttl for other dynamic pages cache-dynamic-ttl=0 # ttl for static pages (addressed by SHA-1) cache-static-ttl=0 # allow download of zip, tar.gz and tar.bz2 files snapshots=tar.gz tar.bz2 zip # repository settings include=/etc/cgitrepos
/etc/cgitrepos:
repo.url=publicrepo repo.desc=Public repository test repo.path=/home/git/repositories/publicrepo.git repo.owner=user
Cобираем все вместе
Теперь осталось только настроить nginx. Сначала укажем параметры кеширования для FastCGI (не забудьте, мы запретили его в настройках cgit). Вставьте следующую строку в секцию http файла /etc/nginx/nginx.conf:
http{ fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=code:10m inactive=1h max_size=100m; ... }
Для удобства информацию о поддомене с cgit мы будем хранить в отдельном конфиге /etc/nginx/sites-available/cgit:
server { listen 80; server_name git.example.com; # Serve static files location ~* ^.+\.(css|png|ico)$ { root /var/www/cgit/static; expires 30d; } location / { rewrite ^/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit?url=$1&$2 last; fastcgi_cache code; fastcgi_cache_valid 200 5m; fastcgi_cache_use_stale off; fastcgi_pass unix:/var/run/spawn-fcgi; fastcgi_read_timeout 5m; fastcgi_index /; fastcgi_param DOCUMENT_ROOT /var/www/cgit; fastcgi_param SCRIPT_FILENAME /var/www/cgit/cgi-bin/cgit; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; } access_log /var/log/nginx/git.example.com/access.log combined; error_log /var/log/nginx/git.example.com/error.log warn; }
Не забудьте создать директорию для логов /var/log/nginx/git.example.com и ссылку на конфиг в /etc/nginx/sites-enabled:
$ ln -s /etc/nginx/sites-available/cgit /etc/nginx/sites-enabled/cgit
Теперь после рестарта nginx при коннекте на http://git.example.com вы должны увидеть стартовую страницу cgit.
Веб-интерфейс в поддиректории
Как я говорил в начале статьи, переход от поддомена к поддиректории не очень сложен. Но, тем не менее, когда мне это вдруг понадобилось, я некоторое время буксовал на конфиге nginx'а. Так что опишу это тоже.
Предположим, мы хотим разместить веб-интерфейс в example.com/git (заметьте, что git- и ssh- доступ по-прежнему указывает на корень сервера; в принципе, можно перенаправить и его, но это создаст избыточность в пути и нужно только если вы используете несколько разных версий git одновременно).
Итак, вносим в конфиги следующие изменения:
/etc/cgitrc:
virtual-root=/git/ ... # link to css file css=/git/cgit.css # link to logo file logo=/git/cgit.png ... # Specify some default clone prefixes clone-prefix=git://example.com ssh://git@example.com http://example.com/git/http
…
/etc/nginx/sites-available/cgit:
server { listen 80; server_name example.com; location ^~ /git/http/ { rewrite ^/git/http/(.*)$ /$1 break; ... } # Serve static files location ~* ^/git/.+\.(css|png|ico)$ { rewrite ^/git/(.*)$ /$1 break; ... } location /git { rewrite ^/git/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit?url=$1&$2 break; ... } access_log /var/log/nginx/example.com/git/access.log combined; error_log /var/log/nginx/example.com/git/error.log warn; }
Заметьте, что: 1. В конфиг nginx'a я добавил исправленный location для http-клонирования из пункта 5.1. 2. В последнем location'е last сменился на break (так как иначе путь /cgit не будет совмещен ни с одним из location'ов). И не забудьте проверить, что пути к логам существуют и имеют нужный доступ.
Бонус-трек
HTTP-клонирование
Чтобы позволить бедным корпоративным пользователям за файрволами клонировать ваш замечательный код по http, надо рассказать nginx-у, где лежат репозитории. Здесь есть два варианта — либо расшарить сразу все, либо только выбранные репозитории. Я расскажу про второй вариант, но отличие первого только в том, что вам надо будет создать одну ссылку вместо нескольких (и настроить rewrite rule, если вам не нравятся постфиксы .git).
Итак, нам надо дать read-only доступ к репозиториям для пользователя www-data. Для этого мы добавим его в группу git и создадим ссылки на публичные репозитории в /var/www:
$ sudo usermod -G git www-data $ sudo mkdir /var/www/git-http $ sudo ln -s /home/git/repositories/publicrepo.git /var/www/git-http/publicrepo $ sudo chown -R www-data:www-data /var/www/git-http
Теперь скажем nginx-у, что он должен перенаправлять пользователя к репозиториям в ответ на запрос к папке http. Добавляем следующие строки в /etc/nginx/sites-available/cgit внутрь блока server (все равно куда, порядок следования location'ов для nginx'a роли не играет):
location ^~ /http/ { rewrite ^/http/(.*)$ /$1 break; root /var/www/git-http; expires 30d; }
Чтобы git при клонировании забирал всегда свежую версию репозитория, необходимо добавить хук для каждого публичного репозитория: /home/git/repositories/publicrepo.git/hooks/post-update:
#!/bin/sh exec git update-server-info
Проверьте, что у хука стоит execution bit и его владельцем является юзер git. Перед первым клонированием хук должен выполниться хотя бы один раз; для этого вы можете сделать push в репозиторий или просто выполнить "git update-server-info" в директории репозитория на сервере.
HTTPS-доступ
Что же делать если нашим гипотетическим пользователям за файрволом недостаточно read-only доступа, а нужна еще и возможность push'а? Те, кто пользуется github'ом, наверняка знают про ssh.github.com, к которому можно обращаться по 443 порту. Признаюсь честно, я не знаю, как именно они это сделали, но мое решение тоже работает.
Итак, нам понадобится маленькая утилитка sslh;, которая будет висеть на 443 порту и распознавать ssh-запросы. Идея состоит в том, что при HTTPS запросе первым должен послать данные клиент, а при SSH запросе — сервер. Поэтому утилитка ждет данных в течение таймаута, и если получает их — отдает соединение веб-серверу, а если нет — ssh-серверу. Установить ее очень просто:
$ sudo aptitude install sslh
Разрешаем ей выполняться, добавив "RUN=yes" в /etc/default/sslh и запускаем сервис (если он уже не запущен):
$ sudo service start sslh
Теперь на клиенте нужно добавить в настройки ~/.ssh/config наш сервер:
Host https.example User git Port 443 Hostname git.example.com PreferredAuthentications publickey # if you use proxy, uncomment this and set proxy address and port: # ProxyCommand corkscrew %h %p
Чтобы использовать туннель, нужно сменить адрес remote'а в настройках существующей локальной копии репозитория на https.example, или склонировать новый репозиторий с использованием нового адреса:
$ git clone git@https.example:publicrepo
Обратите внимание на 2-х секундную задержку при коннекте — это sslh думает, куда направить ваш запрос.
Зеркалирование на github
Свой git-сервер — это, конечно, хорошо, но что делать, если вам нравится github (будь то социальные возможности, интерфейс или статистика)? Или вам нужен HTTPS-доступ, но не хочется покупать платный аккаунт? Не проблема — настроим автоматическое зеркалирование репозиториев с нашего сервера. Для начала, создайте публичный SSH-ключ на сервере и добавьте его к своему аккаунту в github. Затем создайте директорию с нужным репозиторием на сервере: либо склонировав его с github под пользователем git:
$ cd /home/git/repositories $ git clone git@github.com:Username/reponame --bare
либо добавив нужный remote в уже существующий репозиторий:
$ cd /home/git/repositories/reponame $ git remote add github git@github.com:Username/reponame
Также возможны и другие варианты, в зависимости от того, создан ли уже репозиторий на github, существует ли он уже на сервере или на клиенте, но я не буду все их рассматривать, так как приведенной информации достаточно, чтобы все настроить. Не забудьте добавить репозиторий в gitosis.conf (и в /etc/cgitrepos, если необходимо). Чтобы репозиторий зеркалировал себя на github после каждого коммита, добавьте в уже знакомый вам post-update хук строчку "git push –mirror github". Будьте внимательны, по дефолту в этом хуке стоит "exec git update-server-info", где команда exec подменяет существующий процесс — то есть после нее никакие команды исполняться не будут. Если вам хочется оставить и ее, и зеркалирование, то хук будет выглядеть так:
#!/bin/sh git update-server-info git push --mirror github
Все, теперь после push'а клиент увидит, как репозиторий копирует себя на github.