根据 RhodeCode 在 2016 年作过的一项分析报告 Version Control Systems Popularity in 2016,在现在的 VCS(版本控制系统)领域,Git 几乎已经一统江山,在选择本身的 VCS 时,有 87% 的人会选择使用 Git,排在第二名的 SVN 只占 6%,不管是从 Google Trends,仍是在 Stack Overflow 上的提问,均可以看到 Git 的爆发式增加。另外,根据 Eclipse 的社区调查 (Eclipse Community Survey),在 2010 年先后,SVN 的使用率都远超其余几款 VCS,从 2010 年开始,SVN 的使用率开始快速下滑,相应的,Git 的使用率快速上升,并在 2014 年超过了 SVN。php
如今,Git 已经成为了程序员的必备技能,愈来愈多的企业开始采用 Git。在开源的世界里,Github 是程序员汇集的狂欢之地,但这并不适合企业的私有项目,虽然 Github 也支持建立私有项目,可是搭建一个本身的 Git 服务器在不少时候多是更好的选择,这篇博客将介绍并学习几种搭建 Git 服务器的方法。html
Git 支持四种不一样的传输协议:本地协议(Local)、HTTP(S) 协议、SSH(Secure Shell)协议以及 Git 协议,这四种协议在不一样的场合有不一样的用途,而且各有利弊,能够根据实际状况来选择。java
本地协议是 Git 最基本的协议,当咱们想在本地作一些 Git 实验时,这将很是有用。咱们首先创建两个目录:/git/repo
和 ~/working
,前者做为远程版本库,后者做为本地工做目录。nginx
aneasystone@little-stone:~$ sudo mkdir -p /git/repo
aneasystone@little-stone:~$ sudo git init --bare /git/repo/test.git
已初始化空的 Git 仓库于 /git/repo/test.git/
复制代码
咱们在 /git/repo
目录经过 git init --bare
命令建立一个裸仓库(bare repository,即一个不包含当前工做目录的仓库),只要这一步,咱们就能够开始使用了。接着咱们在工做目录 clone
这个版本库:git
aneasystone@little-stone:~$ cd ~/working/
aneasystone@little-stone:~/working$ git clone /git/repo/test.git
正克隆到 'test'...
warning: 您彷佛克隆了一个空仓库。
完成。
复制代码
而后咱们可使用 pull
、push
就像操做其余的版本库同样。程序员
aneasystone@little-stone:~/working$ cd test/
aneasystone@little-stone:~/working/test$ touch 1
aneasystone@little-stone:~/working/test$ touch 2
aneasystone@little-stone:~/working/test$ git add .
aneasystone@little-stone:~/working/test$ git commit -m 'first commit'
[master (根提交) 4983f84] first commit
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1
create mode 100644 2
aneasystone@little-stone:~/working/test$ sudo git push
[sudo] aneasystone 的密码:
对象计数中: 3, 完成.
Delta compression using up to 8 threads.
压缩对象中: 100% (2/2), 完成.
写入对象中: 100% (3/3), 205 bytes | 205.00 KiB/s, 完成.
Total 3 (delta 0), reused 0 (delta 0)
To /git/repo/test.git
* [new branch] master -> master
复制代码
本地协议不只在作 Git 实验时颇有用,若是你的团队有一个共享文件系统,能够在这个共享文件系统上建立一个远程版本库,团队成员把这个共享文件系统挂在本地,就能够直接使用本地协议进行协做开发,彻底不须要搭建一台专门的 Git 服务器。github
本地协议虽然简单,可是通常来讲并不适用,由于你没法控制用户对共享文件系统的操做,用户拥有 push 权限也就意味着用户对远程目录拥有完整的 Shell 权限,他们有可能会无心甚至有意的修改或删除 Git 内部文件,损坏 Git 仓库。算法
更安全的作法是使用专门的 Git 服务器,若是你有一台可使用 SSH 链接的服务器,搭建 Git 服务将会很是简单。首先咱们要确保服务器上运行着 SSH 服务(sshd
),大多数 Linux 服务器版本都默认包含了该服务,若是没有,能够先安装 openssh-server
。而后在服务器上建立 Git 远程版本库:shell
root@myserver:~# mkdir -p /git/repo
root@myserver:~# git init --bare /git/repo/test.git
已初始化空的 Git 仓库于 /git/repo/test.git/
复制代码
而后在本地 clone
这个版本库:apache
aneasystone@little-stone:~/working$ git clone ssh://root@myserver/git/repo/test.git
正克隆到 'test'...
root@myserver's password: warning: 您彷佛克隆了一个空仓库。 复制代码
能够看到和使用本地协议几乎同样,不一样的地方在于,在 clone 的时候须要在 URL 前面加上 ssh://root@myserver
,你也可使用 scp 式的写法:
$ git clone root@myserver:/git/repo/test.git
复制代码
另一点不一样的地方是,每次 pull
、push
的时候都须要输入远程服务器的 root 密码。很显然,让每一个 Git 用户都使用 root 来访问服务器是一种很不安全的作法,有几种方法能够解决这个问题:
adduser
手工管理服务器帐号的麻烦;authorized_keys
文件对用户的公钥进行管理;下面咱们尝试下第三种方法。首先在服务器上建立一个名叫 git 的帐号:
root@myserver:~# adduser git
Adding user `git' ... Adding new group `git' (1000) ...
Adding new user `git' (1000) with group `git' ...
Creating home directory `/home/git' ... Copying files from `/etc/skel' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for git
Enter the new value, or press ENTER for the default
Full Name []: git
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] Y
复制代码
再设置一下 git 仓库的权限(默认状况下,git 仓库的权限为 rwxr-xr-x
,只有建立者 root 有写的权限,也就意味着使用 git 帐号只能 clone
pull
,不能 push
):
# chmod a+w -R /git/repo/test.git
复制代码
咱们这里很是粗暴的使用 chmod a+w
将 git 仓库设置为对全部人可写,这里能够想想,若是咱们但愿设置某些用户对仓库具备只读的权限,该怎么作呢?
而后就能够在本地愉快的进行 git 操做了:
$ git clone git@myserver:/git/repo/test.git
复制代码
到这里彷佛一切都很正常,可是几回实操以后你就会发现,每次 git 操做都要输入一次密码,这也太麻烦了,能不能“免密提交代码”呢?首先咱们要知道,只要能经过 SSH 登录到服务器,咱们就能操做 git,因此若是 SSH 能支持免密登录,咱们就能够“免密提交代码”。还好,SSH 支持公钥认证,这种认证方式无需密码登录。在 Linux 操做系统中,每一个用户均可以拥有本身的一个或多个密钥对(公钥和私钥成对出现),这些密钥通常状况会保存在 ~/.ssh
目录下,在开始以前,咱们先确认下本身是否已经生成过公钥了,能够看下这个目录下是否有 id_dsa.pub
或 id_rsa.pub
这样的文件,若是没有,咱们经过 ssh-keygen
来生成:
aneasystone@little-stone:~/.ssh$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/aneasystone/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/aneasystone/.ssh/id_rsa.
Your public key has been saved in /home/aneasystone/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:4Ulpufuhs/AgDMb0VXnqMUTw6bD/HrAOI2z9c1cod9I aneasystone@little-stone
The key's randomart image is: +---[RSA 2048]----+ | .oo. | | oo+. | | . o.Oo | | o . . B++ | | + . ..So o | | . + . ..+. + E | | * * + oo + | | . o Oo+.o. | | **+. | +----[SHA256]-----+ 复制代码
这样咱们在 ~/.ssh
目录生成了两个文件,id_rsa
是你的私钥,id_rsa.pub
是你的公钥。关于私钥和公钥的原理以及 RSA 加密算法等内容能够参考我以前写过的一篇介绍 HTTPS 和证书 的文章。
咱们假设你的 Git 服务器是由专门的服务器管理员负责维护和管理,当你生成你的公钥以后,就能够给服务器管理员发送一封申请 Git 服务的邮件,并附上你的公钥。服务器管理员在收到你的申请以后,若是赞成了,就能够进行下面的操做:
首先将公钥文件拷贝到服务器上:
# scp id_rsa.pub root@myserver:/home/git
复制代码
将公钥文件的内容追加到 git 帐户的 authorized_keys 文件中(要注意的是,若是是第一次操做,/home/git 目录下是没有 .ssh 目录的,须要手工建立 .ssh 目录和 authorized_keys 文件):
root@myserver:/home/git# cat id_rsa.pub >> /home/git/.ssh/authorized_keys
复制代码
后续若是有其余的用户申请 Git 服务,均可以按照这个步骤操做。一旦完成这个操做,服务器管理员将会回复你的邮件,通知你 Git 服务已经开通,这个时候你再进行 git 操做就能够不用输入密码了。关于 SSH 的使用,更详细的步骤能够参考 Github 上的这篇指南:Connecting to GitHub with SSH。
做为服务器管理员,关于 SSH 还有一点须要考虑,那就是 SSH 的安全问题。在上面介绍本地协议时,咱们说这种方式没法控制用户对 Git 仓库的操做,没法防止用户有意或无心的损坏 Git 仓库,使用 SSH 协议同样存在这样的问题,用户能经过 SSH 拉取和提交代码,也就意味着用户能够经过 SSH 链接到服务器,对 Git 仓库进行任何操做,这是一件很让人担忧的事情。
所以,咱们还须要对 git 帐号作一些限制。默认状况下,咱们新建帐号的登录 shell 是 /bin/bash
,这个配置在 /etc/passwd
文件中:
git:x:1000:1000:git,,,:/home/git:/bin/bash
复制代码
可使用 chsh
命令修改用户的登录 shell,让他不能经过 SSH 访问服务器,怎么修改呢?咱们能够看一下 /etc/shells
文件,这里定义了全部可使用的登录 shell,你能够将 /bin/bash
改为这里的任何一个:
root@myserver:~# cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
复制代码
很显然,这些 shell 并非咱们想要的,有没有一个 shell 只容许用户进行 git 操做,而不容许其余操做呢?还好,Git 的软件包提供了一个名叫 git-shell
的登录 shell,咱们能够把他加进去,通常状况下位于 /usr/bin/git-shell
。咱们使用 chsh
修改 git 的登录 shell:
root@myserver:~# chsh git
Changing the login shell for git
Enter the new value, or press ENTER for the default
Login Shell [/bin/bash]: /usr/bin/git-shell
复制代码
这样当用户 git 经过 SSH 链接服务器时,就会直接被拒绝了。
SSH 协议解决了用户直接操做 Git 仓库的权限问题,可是若是咱们但愿对除仓库维护者以外的全部人都开放 Git 仓库的只读权限,这在开源项目中和企业内部每每是很常见的,任何人均可以去查看仓库的代码,这时管理员须要给每个用户配置 SSH 密钥是很是麻烦的。虽然也可使用变通的方法来达到这个效果,可是很繁琐,下面是具体的步骤:
g+w
设置 Git 仓库的权限,让仓库建立者所在的用户组具备写权限,而不是全部人都有写权限(这一步一般也能够在 git init
的时候加上 --shared
参数);能够看到使用 SSH 协议最终都逃不过受权这一步,并且公开私钥的作法也不是很优雅。实际上,Git 提供了另外一种方式来让这个操做更简单,那就是 Git 协议。使用 Git 协议必需要在服务器上运行 Git 守护进程,git 命令自带了一个 daemon
参数:
root@myserver:~# git daemon --reuseaddr --base-path=/git/repo/ /git/repo/
复制代码
上面的各个参数能够 参考 git-daemon 的文档。git-daemon 会监听 9418 端口,若是你的服务器有防火墙,须要将该端口添加到白名单,若是你使用的是阿里云服务器,须要像下面这样添加一个安全组规则:
为了让全部的用户均可以访问咱们的仓库,还须要在仓库目录下建立一个名为 git-daemon-export-ok
的文件:
root@myserver:~# cd /git/repo/test.git/
root@myserver:/git/repo/test.git/# touch git-daemon-export-ok
复制代码
至此,全部人均可以经过 Git 协议来克隆或拉取项目源码了(注意上面指定了 base-path
参数为 /git/repo/
,因此 URL 能够直接写 git://myserver/test.git
):
aneasystone@little-stone:~/working$ git clone git://myserver/test.git
复制代码
通常状况下,服务器管理员还会作一些其余的配置,譬如在服务器重启时让 Git 守护进程自动启动,这有不少种方式能够实现,能够参考《Pro Git》 Git 守护进程 这一节的内容。
咱们通常经过 Git 协议进行无受权访问,经过 SSH 协议进行受权访问,若是你的项目是内部项目,只针对部分受权用户,那使用 SSH 协议就足够了,可是若是既须要受权访问也须要无受权访问,可能须要 SSH 协议和 Git 协议搭配使用,这在维护上成本很高。这时就到了咱们的压轴戏 —— HTTP 协议出场的时候了,它同时支持上面两种访问方式。
经过 HTTP 协议访问 Git 服务是目前使用最普遍的方式,它支持两种模式:旧版本的 Dumb HTTP
和 新版本的 Smart HTTP
,Dumb HTTP 通常不多使用,下面除非特殊说明,所说的 HTTP 协议都是 Smart HTTP。使用 HTTP 协议的好处是可使用各类 HTTP 认证机制,好比用户名/密码,这比配置 SSH 密钥要简单的多,对普通用户来讲也更能接受。若是担忧数据传输安全,也能够配置 HTTPS 协议,这和普通的 Web 服务是同样的。
下面咱们就来尝试搭建一个基于 HTTP 协议的 Git 服务器。《Pro Git》上提供了一个基于 Apache 的配置示例,若是你是使用 Apache 做为 Web 服务器,能够参考之,咱们这里使用 Nginx 来做为 Web 服务器,其原理本质上是同样的,都是经过 Web 服务器接受 HTTP 请求,并将请求转发到 Git 自带的一个名为 git-http-backend
的 CGI 脚本。
首先咱们安装所需的软件:
# apt-get install -y git-core nginx fcgiwrap apache2-utils
复制代码
其中,Nginx 做为 Web 服务器,自己是不能执行外部 CGI 脚本的,须要经过 fcgiwrap 来中转,就像使用 php-fpm 来执行 PHP 脚本同样。apache2-utils 是 Apache 提供的一个 Web 服务器的工具集,包含了一些有用的小工具,譬以下面咱们会用到的 htpasswd 能够生成 Basic 认证文件。
启动 nginx 和 fcgiwrap,并访问 http://myserver
测试 Web 服务器是否能正常访问:
# service nginx start
# service fcgiwrap start
复制代码
而后咱们打开并编辑 Nginx 的配置文件(/etc/nginx/sites-available/default
):
location / {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
复制代码
这里经过 fastcgi_param
设置了一堆的 FastCGI 参数,以下:
git-http-backend
的位置,表示每次 HTTP 请求会被转发到该 CGI 脚本;git-http-backend
默认只能访问目录下有 git-daemon-export-ok
文件的 Git 仓库,和上面介绍的 Git 协议是同样的,若是指定了 GIT_HTTP_EXPORT_ALL,表示容许访问全部仓库;改完以后咱们重启 Nginx,并经过 HTTP 协议 clone
仓库:
aneasystone@little-stone:~/working$ git clone http://myserver/test.git
复制代码
到这里一切 OK,可是当咱们 push
代码的时候,却会报下面的 403 错误:
aneasystone@little-stone:~/working/test$ git push origin master
fatal: unable to access 'http://myserver/test.git/': The requested URL returned error: 403
复制代码
为了解决这个错误,咱们能够在 git-http-backend 的官网文档 上找到这样的一段描述:
By default, only the
upload-pack
service is enabled, which serves git fetch-pack and git ls-remote clients, which are invoked from git fetch, git pull, and git clone. If the client is authenticated, thereceive-pack
service is enabled, which serves git send-pack clients, which is invoked from git push.
第一次读这段话可能会有些不知所云,这是由于咱们对这里提到的 upload-pack
、fetch-pack
、receive-pack
、send-pack
这几个概念尚未什么认识。可是咱们大抵能够猜出来,默认状况下,只有认证的用户才能够 push 代码,若是某个 Git 仓库但愿全部用户都有权限 push 代码,能够为相应的仓库设置 http.receivepack
:
root@myserver:/# cd /git/repo/test.git/
root@myserver:/git/repo/test.git# git config http.receivepack true
复制代码
固然最好的作法仍是对 push 操做开启认证,官网文档上有一个 lighttpd 的配置 咱们能够借鉴:
$HTTP["querystring"] =~ "service=git-receive-pack" {
include "git-auth.conf"
}
$HTTP["url"] =~ "^/git/.*/git-receive-pack$" {
include "git-auth.conf"
}
复制代码
这个配置看上去很是简单,可是想要理解为何这样配置,就必须去了解下 Git 的内部原理。正如上面 git-http-backend 文档上的那段描述,当 Git 客户端执行 git fetch, git pull, and git clone 时,会调用 upload-pack
服务,当执行 git push 时,会调用 receive-pack
服务,为了更清楚的说明这一点,咱们来看看 Nginx 的访问日志。
执行 git clone
:
[27/Nov/2018:22:18:00] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:18:00] "POST /test.git/git-upload-pack HTTP/1.1" 200 306 "-" "git/1.9.1"
复制代码
执行 git pull
:
[27/Nov/2018:22:20:25] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:20:25] "POST /test.git/git-upload-pack HTTP/1.1" 200 551 "-" "git/1.9.1"
复制代码
执行 git push
:
[27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 401 204 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 200 193 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "POST /test.git/git-receive-pack HTTP/1.1" 200 63 "-" "git/1.9.1"
复制代码
能够看到执行 clone 和 pull 请求的接口是同样的,先请求 /info/refs?service=git-upload-pack
,而后再请求 /git-upload-pack
;而 push 是先请求 /info/refs?service=git-receive-pack
,而后再请求 /git-receive-pack
,因此在上面的 lighttpd 的配置中咱们看到了两条记录,若是要对 push 作访问控制,那么对这两个请求都要限制。关于 Git 传输的原理能够参考 《Pro Git》的 Git 内部原理 - 传输协议 这一节。
咱们依葫芦画瓢,Nginx 配置文件以下:
location @auth {
auth_basic "Git Server";
auth_basic_user_file /etc/nginx/passwd;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
location / {
error_page 418 = @auth;
if ( $query_string = "service=git-receive-pack" ) { return 418; }
if ( $uri ~ "git-receive-pack$" ) { return 418; }
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
复制代码
其中相同的配置咱们也能够用 include
指令放在一个共用的配置文件里,这样咱们就实现了在 push 的时候须要填写用户名和密码了。咱们经过 Nginx 的 auth_basic_user_file
指令来作身份认证,用户名和密码保存在 /etc/nginx/passwd
文件中,这个文件可使用上面提到的 apache2-utils 包里的 htpasswd 来生成:
root@myserver:/# htpasswd -cb /etc/nginx/passwd admin 123456
复制代码
另外,在 push 的时候,有时候可能会遇到 unpack failed: unable to create temporary object directory
这样的提示错误:
aneasystone@little-stone:~/working/test$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 193 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
error: unpack failed: unable to create temporary object directory
To http://myserver/test.git
! [remote rejected] master -> master (unpacker error)
error: failed to push some refs to 'http://myserver/test.git'
复制代码
这通常状况下都是因为 Git 仓库目录的权限问题致使的,在这里 Git 仓库的根目录 /git/repo
是 root 建立的,而运行 nginx 和 fcgiwrap 的用户都是 www-data,咱们能够把 Git 仓库目录设置成对全部人可读可写,也能够像下面这样将它的拥有者设置成 www-data 用户:
root@myserver:/# chown -R www-data:www-data /git/repo
复制代码
上面咱们站在管理员的角度解决了用户身份认证的问题,可是站在用户的角度,每次提交代码都要输入用户名和密码是一件很痛苦的事情。在上面介绍 SSH 协议时,咱们可使用 SSH 协议自带的公钥认证机制来省去输入密码的麻烦,那么在 HTTP 协议中是否存在相似的方法呢?答案是确定的,那就是 Git 的凭证存储工具:credential.helper
。
譬如像下面这样,将用户名和密码信息保存在缓存中:
$ git config --global credential.helper cache
复制代码
这种方式默认只保留 15 分钟,若是要改变保留的时间,能够经过 --timeout
参数设置,或者像下面这样,将密码保存在文件中:
$ git config --global credential.helper store
复制代码
这种方式虽然能够保证密码不过时,可是要记住的是,这种方式密码是以明文的方式保存在你的 home 目录下的。能够借鉴操做系统自带的凭证管理工具来解决这个问题, 好比 OSX Keychain 或者 Git Credential Manager for Windows。更多的内容能够参考《Pro Git》凭证存储 这一节。
除此以外,还有一种更简单粗暴的方式:
aneasystone@little-stone:~/working$ git clone http://admin:123456@myserver/test.git
复制代码
这一节对 Git 的四大协议作一个综合对比。
上面介绍的是搭建 Git 服务器最基本的方法,若是你只是但愿能找一个版本控制系统来替代现有的 SVN,这也许就足够了。但若是你但愿你的版本控制系统能拥有一个更友好的 UI 界面,能更好的管理你的用户和权限,能支持更现代的 Pull Request 功能以及能和 CI/CD 系统更紧密的联系起来,你就须要一个更高级的工具,你能够试试 GitWeb、Gitolite、Gitlab、Gogs、Gitea,固然,若是你愿意,你也能够把代码放在那些流行的代码托管平台上,好比 Github、Bitbucket 等等。