现在我们将开始学习如何构建一个Git服务器,如何在具体的事件中写一个针对特定的触发操作的自定义Git(例如通告),如何发布你的代码到一个网站。
目前为止,用户对Git的焦点主要在Git的使用上。这篇文章中,我将论述如何管理Git以及自定Git架构的设计。你可能会觉得这听起来像“先进的Git技术”或者“如果你是学霸读这篇文章”的委婉说法,但是事实上理解Git如何工作不需要任何的高深知识或者专业培训,在一些情况下只需要了解一点Linux。
分享Git服务
创建自己的Git服务共享非常简单, 在许多情况下值得去创建这样一个Git服务。它不仅保证你随时可以访问你的代码,它的无限制数据存储、持续集成跟部署同时为个人Git管理打开了一道Git延伸与拓展的大门。
假如你知道如何使用Git和SSH,那么你已经知道如何创建一个Git服务器了。设计Git的方式,你创建或者克隆一个仓库时,你已经创建了一半服务器。使能SSH访问仓库,并且任何访问你仓库的人都可以使用你的回购协议作为一个新克隆的基础。
但是,会有一个小特设。有些计划你可以构建关于同样数量的精心设计的Git服务器,但是可以具有更好扩展性。
首先是:识别你的用户,包括闲杂与未来的。假如你是唯一用户,那么无需任何改变,但是如果你邀请国外的贡献者了,那么你应该为开发者搭建一个贡献共享系统平台。
假定你有一个可用的服务器(如果不是,Git无法解决这个问题,但是运行在Raspberry Pi 3 的CentOS将会是一个良好开端),第一步是采用SSH键值授权登录,它比密码登录更加强大,因为它能免疫于蛮力攻击,并且可以避免用户尽可能简单地删除它们的键值。
在你启用了SSH密钥认证之后,就创建一个 gituser用户。这是一个提供给所有通过了认证的用户的共享用户账号:
1 |
$ su -c 'adduser gituser' |
然后切换到这个用户,并使用合适的权限创建一个 ~/.ssh 框架。这非常重要,因为如果权限设置太过于随意,你自己针对SSH的防护默认就会失效。
2 |
$ mkdir . ssh && chmod 700 . ssh |
3 |
$ touch . ssh /authorized_keys |
4 |
$ chmod 600 . ssh /authorized_keys |
authorized_keys 文件里面有所有你赋予其权限操作你的Git工程的开发者的SSH公共密钥。你的开发者必须创建属于他们自己的SSH密钥并将其中的公共密钥发送给你。要把这些公共密钥复制到gituser的 authorized_keys 文件中去。例如,对于一个叫做Bob的开发者,可以运行这些命令:
1 |
$ cat ~/path/to/id_rsa.bob.pub >> \ |
2 |
/home/gituser/. ssh /authorized_keys |
当开发者Bob持有能匹配他发送给你的公共密钥的私有密钥时,他就能以gituser访问服务器。
不过,你并不会真的想让你的开发者访问到服务器,即使只是以gituser用户来进行访问。你想要的是让他们只能访问到Git资源库。因为这个原因,Git提供了一个受限的shell,恰如其分的将其称为
git-shell、以root用户运行下面的这些命令可以将git-shell添加到你的系统中,并使其成为gituser用户的默认shell:
1 |
# grep git-shell /etc/shells || su -c \ |
2 |
"echo `which git-shell` >> /etc/shells" |
3 |
# su -c 'usermod -s git-shell gituser' |
现在gituser只能使用SSH来向Git资源库进行推送和拉取操作,而不能访问到一个登陆shell。你应该将你自己加入gituser对应的用户组,在我们的示例服务器中它还是gituser。
例如:
1 |
# usermod -a -G gituser seth |
剩下的唯一一个步骤就是创建一个Git资源库。因为不会有人在服务器上跟它进行直接交互(也就是说你不会通过SSH连上服务器然后直接在资源库中进行操作),
这使其成为了一个基础的资源库。如果你想要把服务器上的资源库用起来,就要将其从它所在的地方克隆到自己的home目录中去。
严格来说,你并不用使其成为一个基础资源库,它还是可以作为一个普通的资源库来操作的。不过,一个基础资源库是没有*工作树(working
tree)* (也就是说,不会有分支会处在”checkout“状态)。这很重要,因为远程用户不会被允许向一个活动分支进行推送 (你是不会想在一个”dev“分支工作时突然有人将变更推送到你的工作空间的?)。因为基础资源库不能有活动分支,那就不会有问题发生了。
你可以将资源库放到任何你想要放置的地方, 只要你想赋予权限的用户和组也能访问到它就行了。你不会想将目录存储到一个用户的home目录的,因为这里的权限相当地严格,
而是要放在一个通用共享的位置,例如 /opt or /usr/local/share.
以root用户创建一个基础资源库:
1 |
# git init --bare /opt/jupiter.git |
2 |
# chown -R gituser:gituser /opt/jupiter.git |
3 |
# chmod -R 770 /opt/jupiter.git |
现在任何已gituser认证的、或者是位于gituser分组的用户都可以读取和写入jupiter.git资源库。你可以在自己本机上试试看:
1 |
$ git clone gituser@example.com:/opt/jupiter.git jupiter.clone |
2 |
Cloning into 'jupiter.clone' ... |
3 |
Warning: you appear to have cloned an empty repository. |
记住:开发有必须让他们的公共SSH密钥导入gituser用户的 authorized_keys 文件, 或者是拥有服务器上面的账户(就像你一样),
那样的话他们就必须是gituser组的成员。
Git钩子
运行你自己的Git服务器带来的一个好处是它提供了Git钩子。Git托管服务有时也提供了一个类似钩子的接口,但那并不是真正的可以访问文件系统的Git钩子。一个Git钩子是一个脚本,它在Git进程中的某个时刻执行。在一个仓库(repository)接受一个提交(commit)之前,或者收到一个提交之后,或者接收一个推送(push)之前,或者收到一个推送之后等时刻执行一个钩子。
这个系统很简单:任何可执行的脚本都存放在.git/hooks目录中,使用标准的命名方案,并且在某个指定的时刻执行。脚本执行的时间由名字来决定;pre-push脚本在推送之前执行,post-receive脚本在收到一个提交之后执行,诸如此类。它基本上属于自文档(self-documenting)。
可以使用任何语言编写钩子脚本;如果你能在你的系统上运行某种语言的hello world脚本,那么你就可以使用那门语言来编写Git钩子脚本。默认情况下,Git附带了一些范例,但没有启用。
想要运行一个脚本吗?使用起来很简单。如果你还没有Git仓库的话,首先创建一个。
然后编写一个"hello world" Git钩子。由于在工作中我为了传统支持而使用tcsh,所以我坚持使用它作为我的脚本语言,但你可以自由地选用你喜爱的语言(Bash、Python、Ruby、Perl、Rust、Swift、Go):
1 |
$ echo "#\!/bin/tcsh" > .git/hooks/post-commit |
2 |
$ echo "echo 'POST-COMMIT SCRIPT TRIGGERED'" > \ |
3 |
~/jupiter/.git/hooks/post-commit |
4 |
$ chmod +x ~/jupiter/.git/hooks/post-commit |
现在进行测试:
1 |
$ echo "hello world" > foo.txt |
3 |
$ git commit -m 'first commit' |
4 |
! POST-COMMIT SCRIPT TRIGGERED |
5 |
[master (root-commit) c8678e0] first commit |
6 |
1 file changed, 1 insertion(+) |
7 |
create mode 100644 foo.txt |
这就是你的第一个可以正常运行的Git钩子。
著名的推送到web
钩子
一个流行的Git钩子用法是自动推送改变部分到工作生产中的web服务器目录。这是一个伟大构建FTP的方式,保留开发环节的全版本控制,并且整合、自动化发布内容。
如果正确执行,它将会工作运行良好,在某一种程度来说,一直应该做的是考虑如何网络发布。它是不错的。我不知道最初是谁想出这主意的,但是我第一次是从EMacs和来自IBM公司的Git-mentor、Bill
Von Hagen那里听到的。他的文章仍然是对这个过程起决定性作用的介绍:Git 改变分布式Web开发规则。
Git变量
每个Git钩子获取一组不同的Git动作触发它的相关变量。你可能会用到这些变量,也可能用不到。这取决于你所写的作品。 如果你想要的是一个普通的邮件来通知你,有人推了东西。那么你不需要细节,也不需要脚本,因为你可以套用现有的样板。如果你想在邮件里浏览别人提交的信息和作者,那么对你的脚本要求更高。
Git钩子并不是用户直接运行的,所以要理解透如何获取这些混乱却重要的信息。事实上,一个Git钩子脚本与其他任何脚本类似,像BASH、Python、C++或者其他脚本一样的方式接受来自stdin的参数。不同的是,我们不会提供自己入参,所以使用它时要弄清楚你想要的是什么(参数)。
编写一个Git钩子之前,可以进入到你的项目目录.git/hooks查看Git提供的范例。例如,下面是pre-push.sample文件的注释部分:
1 |
# $1 -- Name of the remote to which the push is being done |
2 |
# $2 -- URL to which the push is being done |
3 |
# If pushing without using a named remote those arguments will be equal. |
5 |
# Information about commit is supplied as lines |
6 |
# to the standard input in this form: |
7 |
# <local ref> <local sha1> <remote ref> <remote sha1> |
并非所有的范例写的都那么清晰,文档对于什么钩子需要什么变量的说明还有些不足(除非你要阅读Git的源代码),不过若有疑问,你可以通过trials
of other users进行更多地了解,或者编写一个简单的脚本,输出$1、$2、$3等。
分支检测范例
我发现在实际的生产中对钩子最常见的需求是针对受影响的分支触发特定的事件。下面这个例子演示了如何解决这样的任务。
首先,Git钩子本身不是版本控制。也就是说,Git不会跟踪它自己的钩子,因为Git钩子是Git的组成部分而不是你的仓库的一部分。因此,Git钩子在监视提交和推送的同时,可能对你的Git服务器上的远程仓库最有意义,而不是作为你的本地仓库的一部分。
我们来编写一个基于post-receive运行的钩子(即,收到一个提交之后)。第一步是识别分支名字:
for循环读第一个参数($1),然后再次循环读入第二个参数值($2),接着用第三个参数($3)再次循环。在Bash中有更好的方式:使用read命令,将这些值放入一个数组。但是,这里使用的是tcsh,并且变量顺序是预先定义好的,这样做要安全些。
当有即将提交的refname时,我们可以使用Git来获取可读性的分支名:
1 |
set branch = `git rev-parse --symbolic --abbrev-ref $refname` |
这时我们可以将分支名与基于动作名的关键字进行比较:
01 |
if ( "$branch" == "master" ) then |
02 |
echo "Branch detected: master" |
04 |
--work-tree=/path/to/where/you/want/to/copy/stuff/to \ |
05 |
checkout -f $branch || echo "master fail" |
06 |
else if ( "$branch" == "dev" ) then |
07 |
echo "Branch detected: dev" |
09 |
--work-tree=/path/to/where/you/want/to/copy/stuff/to \ |
10 |
checkout -f $branch || echo "dev fail" |
12 |
echo "Your push was successful." |
13 |
echo "Private branch detected. No action triggered." |
授予脚本执行权限:
1 |
$ chmod +x ~/jupiter/.git/hooks/post-receive |
当用户提交代码到主服务器上的分支时,这些代码会被复制到生产环境的目录中,当代码提交到开发环境上的分支时,这些代码会被复制到其它地方,而提交到其它任何分支上时不会触发复制的操作。
检测某人是否正在尝试将代码提交到本不应该提交的分支上,或解析提交准许日志,等等的这些操作都像预提交脚本一样简单。
Git的钩子(Hooks)能够处理复杂的事情,同时它通过利用Git工作的抽象层让人费解,但它是一个强大的系统,允许你设计所有Git基础设施中的动作。如果你只是想要熟悉这个过程,那你可以简单研究一下,如果你是一个严格的Git用户或是全职的Git管理员,那你得要深研了!
在接下来的也就是此系列最后一篇文章,我们将学习怎么通过Git来管理非文本的大文件,如音频文件、图像文件。