Собственный git-сервер с нуля

Обновленная статья доступна на английском. Возможно, позже я приведу русский вариант в соответствие с ней.

Если вы активно программируете, то наверняка пользуетесь какой-либо системой контроля версий. Возможно, вы уже задумывались о том, чтобы перенести все свои репозитории с локальной машины/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.