如何构建你自己的 Git 服务器

4 位读者喜欢这篇文章。
Server room

Cory Doctorow。由 Opensource.com 修改。CC BY-SA 2.0。

阅读

现在我们将学习如何构建 Git 服务器,以及如何编写自定义 Git 钩子,以便在特定事件(例如通知)上触发特定操作,以及将你的代码发布到网站。

到目前为止,重点一直是作为用户与 Git 交互。在本文中,我将讨论 Git 的管理以及灵活的 Git 基础设施的设计。你可能会认为这听起来像是“高级 Git 技术”或“只有超级书呆子才读这个”的委婉说法,但实际上,这些任务都不需要高级知识或任何特殊培训,只需要对 Git 的工作原理有中级理解,在某些情况下,还需要一点 Linux 知识。

共享 Git 服务器

创建你自己的共享 Git 服务器非常简单,并且在许多情况下非常值得付出努力。它不仅确保你始终可以访问你的代码,还为通过扩展来扩展 Git 的功能打开了大门,例如个人 Git 钩子、无限数据存储以及持续集成和部署。

如果你知道如何使用 Git 和 SSH,那么你已经知道如何创建 Git 服务器。根据 Git 的设计方式,当你创建或克隆仓库的那一刻,你已经设置了一半的服务器。然后启用对仓库的 SSH 访问,任何有权访问的人都可以使用你的仓库作为新克隆的基础。

然而,这有点临时。通过一些规划,你可以构建一个设计良好的 Git 服务器,付出大致相同的努力,但具有更好的可扩展性。

首先要做的:确定你的用户,包括当前用户和未来用户。如果你是唯一的用户,则无需进行任何更改,但如果你打算邀请贡献者加入,那么你应该为你的开发人员考虑使用专用的共享系统用户。

假设你有一个可用的服务器(如果没有,这并不是 Git 可以解决的问题,但树莓派 3 上的 CentOS 是一个不错的开始,https://wiki.centos.org/SpecialInterestGroup/AltArch/Arm32/RaspberryPi3),那么第一步是仅使用 SSH 密钥授权启用 SSH 登录。这比密码登录强大得多,因为它对暴力攻击免疫,并且禁用用户就像删除他们的密钥一样简单。

启用 SSH 密钥授权后,创建 gituser。这是一个供所有授权用户使用的共享用户

$ su -c 'adduser gituser'

然后切换到该用户,并使用适当的权限创建 ~/.ssh 框架。这很重要,因为为了保护你自己,如果你设置的权限过于宽松,SSH 将默认为失败

$ su - gituser
$ mkdir .ssh && chmod 700 .ssh
$ touch .ssh/authorized_keys
$ chmod 600 .ssh/authorized_keys

authorized_keys 文件保存了你授予 Git 项目工作权限的所有开发人员的 SSH 公钥。你的开发人员必须创建他们自己的 SSH 密钥对,并将他们的公钥发送给你。将公钥复制到 gituser 的 authorized_keys 文件中。例如,对于名为 Bob 的开发人员,运行以下命令

$ cat ~/path/to/id_rsa.bob.pub >> \ 
/home/gituser/.ssh/authorized_keys

只要开发人员 Bob 拥有与他发送给你的公钥匹配的私钥,Bob 就可以作为 gituser 访问服务器。

但是,你实际上并不想给你的开发人员访问你的服务器的权限,即使只是作为 gituser。你只想给他们访问 Git 仓库的权限。为此,Git 提供了一个有限的 shell,恰如其分地称为 git-shell。以 root 用户身份运行以下命令,将 git-shell 添加到你的系统,然后将其设置为 gituser 的默认 shell

# grep git-shell /etc/shells || su -c \
"echo `which git-shell` >> /etc/shells"
# su -c 'usermod -s git-shell gituser'

现在 gituser 只能使用 SSH 推送和拉取 Git 仓库,而无法访问登录 shell。你应该将你自己添加到 gituser 的相应组中,在我们的示例服务器中,该组也是 gituser。

例如

# usermod -a -G gituser seth

剩下的唯一步骤是创建一个 Git 仓库。由于没有人会在服务器上直接与它交互(也就是说,你不会 SSH 到服务器并直接在这个仓库中工作),因此将其设为裸仓库。如果你想在服务器上使用该仓库来完成工作,你将从它所在的位置克隆它,并在你的主目录中工作。

严格来说,你不必将其设为裸仓库;它可以作为普通仓库工作。但是,裸仓库没有工作树(也就是说,任何分支都不会处于 `checkout` 状态)。这很重要,因为不允许远程用户推送到活动分支(如果你正在 `dev` 分支中工作,突然有人将更改推送到你的工作区,你感觉如何?)。由于裸仓库不能有活动分支,因此永远不会出现问题。

你可以将此仓库放置在你喜欢的任何位置,只要你想要授予访问权限的用户和组可以这样做即可。例如,你希望将目录存储在用户的主目录中,因为那里的权限非常严格,而是存储在常见的共享位置,例如 /opt/usr/local/share

以 root 用户身份创建一个裸仓库

# git init --bare /opt/jupiter.git
# chown -R gituser:gituser /opt/jupiter.git
# chmod -R 770 /opt/jupiter.git

现在,任何以 gituser 身份验证的用户或属于 gituser 组的用户都可以读取和写入 jupiter.git 仓库。在本地机器上试用一下

$ git clone gituser@example.com:/opt/jupiter.git jupiter.clone
Cloning into 'jupiter.clone'...
Warning: you appear to have cloned an empty repository.

记住:开发人员必须将其公共 SSH 密钥输入到 gituser 的 authorized_keys 文件中,或者如果他们在服务器上拥有帐户(就像你一样),则他们必须是 gituser 组的成员。

Git 钩子

运行你自己的 Git 服务器的一个好处是它可以使 Git 钩子可用。Git 托管服务有时会提供类似钩子的界面,但它们不会给你真正的 Git 钩子,让你访问文件系统。Git 钩子是一个脚本,它在 Git 进程的某个点执行;钩子可以在仓库即将接收提交时执行,或者在它接受提交后执行,或者在它接收推送之前执行,或者在推送之后执行,等等。

这是一个简单的系统:任何放置在 .git/hooks 目录中,使用标准命名方案的可执行脚本,都会在指定的时间执行。脚本应该在何时执行由名称决定;pre-push 脚本在推送之前执行,post-receive 脚本在收到提交后执行,依此类推。它或多或少是自文档化的。

脚本可以用任何语言编写;如果可以在你的系统上执行语言的 hello world 脚本,那么你就可以使用该语言编写 Git 钩子脚本。默认情况下,Git 附带一些示例,但没有任何启用。

想看看实际效果吗?入门很容易。首先,创建一个 Git 仓库(如果你还没有的话)

$ mkdir jupiter
$ cd jupiter
$ git init .

然后编写一个“hello world”Git 钩子。由于我在工作中为了遗留支持而使用 tcsh,因此我将继续使用它作为我的脚本语言,但你可以随意使用你喜欢的语言(Bash、Python、Ruby、Perl、Rust、Swift、Go)来代替。

$ echo "#\!/bin/tcsh" > .git/hooks/post-commit
$ echo "echo 'POST-COMMIT SCRIPT TRIGGERED'" >> \
~/jupiter/.git/hooks/post-commit
$ chmod +x ~/jupiter/.git/hooks/post-commit

现在测试一下

$ echo "hello world" > foo.txt
$ git add foo.txt
$ git commit -m 'first commit'
! POST-COMMIT SCRIPT TRIGGERED
[master (root-commit) c8678e0] first commit
1 file changed, 1 insertion(+)
create mode 100644 foo.txt

这就是你的第一个功能正常的 Git 钩子。

著名的 push-to-web 钩子

Git 钩子的一种流行用途是自动将更改推送到实时的生产 Web 服务器目录。这是一种摆脱 FTP、保留对生产环境内容的完整版本控制以及集成和自动化内容发布的好方法。

如果做得正确,它会非常出色地工作,并且在某种程度上,这正是 Web 发布应该一直以来的方式。它就是那么好。我不知道最初是谁想出了这个主意,但我第一次听说它来自我在 IBM 的 Emacs 和 Git 导师 Bill von Hagen。他的文章仍然是对该过程的权威介绍:Git 改变了分布式 Web 开发的游戏规则

Git 变量

每个 Git 钩子都获得一组与触发它的 Git 操作相关的不同变量。你可能需要也可能不需要使用这些变量;这取决于你编写的内容。如果你想要的只是一个通用的电子邮件,提醒你有人推送了某些东西,那么你不需要具体信息,甚至可能不需要编写脚本,因为现有的示例可能对你有效。如果你想在电子邮件中看到提交消息和提交作者,那么你的脚本就会变得更加复杂。

Git 钩子不是由用户直接运行的,因此弄清楚如何收集重要信息可能会令人困惑。实际上,Git 钩子脚本就像任何其他脚本一样,以与 BASH、Python、C++ 和任何其他脚本相同的方式从 stdin 接受参数。不同之处在于,我们没有自己提供输入,因此要使用它,你需要知道期望什么。

在编写 Git 钩子之前,请查看 Git 在你的项目的 .git/hooks 目录中提供的示例。例如,pre-push.sample 文件在注释部分中说明

# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
# If pushing without using a named remote those arguments will be equal.
#
# Information about commit is supplied as lines
# to the standard input in this form:
# <local ref> <local sha1> <remote ref> <remote sha1>

并非所有示例都那么清楚,并且关于哪个钩子获得哪个变量的文档仍然有点稀疏(除非你想阅读 Git 的源代码),但如果有疑问,你可以从 其他用户的尝试 中学到很多在线知识,或者只是编写一个基本脚本并回显 $1$2$3 等。

分支检测示例

我发现生产实例中的一个常见需求是根据受影响的分支触发特定事件的钩子。这是一个关于如何处理此类任务的示例。

首先,Git 钩子本身不受版本控制。也就是说,Git 不会跟踪自己的钩子,因为 Git 钩子是 Git 的一部分,而不是你的仓库的一部分。因此,监视提交和推送的 Git 钩子可能最适合放在你的 Git 服务器上的裸仓库中,而不是作为你的本地仓库的一部分。

让我们编写一个在 post-receive 时运行的钩子(也就是说,在收到提交之后)。第一步是识别分支名称

#!/bin/tcsh

foreach arg ( $< )
  set argv = ( $arg )
  set refname = $1
end

这个 for 循环读取第一个参数 ($1),然后再次循环以用第二个 ($2) 的值覆盖它,然后再用第三个 ($3) 的值覆盖它。在 Bash 中有更好的方法来做到这一点:使用 read 命令并将值放入数组中。但是,由于这是 tcsh 并且变量顺序是可预测的,因此可以安全地破解它。

当我们有了正在提交的内容的 refname 时,我们可以使用 Git 来发现分支的人类可读名称

set branch = `git rev-parse --symbolic --abbrev-ref $refname`
echo $branch #DEBUG

然后将分支名称与我们想要作为操作基础的关键字进行比较

if ( "$branch" == "master" ) then
  echo "Branch detected: master"
  git \
    --work-tree=/path/to/where/you/want/to/copy/stuff/to \
    checkout -f $branch || echo "master fail"
else if ( "$branch" == "dev" ) then
  echo "Branch detected: dev"
  Git \
    --work-tree=/path/to/where/you/want/to/copy/stuff/to \
    checkout -f $branch || echo "dev fail"
  else
    echo "Your push was successful."
    echo "Private branch detected. No action triggered."
endif

使脚本可执行

$ chmod +x ~/jupiter/.git/hooks/post-receive

现在,当用户提交到服务器的 master 分支时,代码会被复制到生产目录中,提交到 dev 分支的代码会被复制到其他位置,而任何其他分支都不会触发任何操作。

创建一个 pre-commit 脚本也很简单,例如,检查是否有人试图推送到他们不应该推送的分支,或者解析提交消息以查找批准字符串等等。

Git 钩子可能会变得复杂,并且由于通过 Git 施加的抽象级别而可能令人困惑,但它们是一个强大的系统,允许你在 Git 基础设施中设计各种操作。它们值得涉足,即使只是为了熟悉该过程,如果你是认真的 Git 用户或全职 Git 管理员,则值得掌握它们。

在本系列的下一篇也是最后一篇文章中,我们将学习如何使用 Git 管理非文本二进制大对象,例如音频和图形文件。

标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,经常同时从事这两个行业。

13 条评论

不错的文章 Seth,谢谢。提示:要了解更多关于 git 的信息,你可以从 https://git-scm.cn/book/en/v2 下载 Pro Git 电子书的免费副本

我期待你的下一篇关于使用 git 管理二进制大对象的文章。希望它包括 dist-git 以及安装/配置步骤。

如果您想简化此操作,可以在服务器上安装 gitolite 软件包。它极大地帮助用户/密钥管理和权限管理。

很棒的系列文章。对我来说尤其及时,因为我一直在 Synology NAS 上设置自己的 git 服务器。感谢这个有用的系列文章!

现在来谈谈“git pick”(抱歉,忍不住想说一下)

在创建 post-commit 脚本的示例中,我认为第二行在行继续符之前应该使用 >> 而不是 >,这样 echo 行就会附加到 post-commit 脚本中,而不是覆盖它。丢失 tcsh 的显式调用是无害的,因为在几乎任何 shell 中,不带标志的 echo 工作方式都相同 :-)

你引用了

“您不希望将目录存储在用户的 home 目录中,例如,因为那里的权限非常严格,而是存储在公共共享位置,例如 /opt 或 /usr/local/share。”

我认为 /home/gituser 拥有正确的权限来通过 ssh 执行任何 git 操作(clone、push、pull、fetch 等)。

我向新管理员推荐这样做,因为我注意到其他情况下权限方面存在一些小的障碍。与 git 或 ssh 无关,所以也许我不应该在本文中提到它;无论如何,我想这最终都取决于本地系统的配置方式。这就是写这些东西如此有趣的原因:因为一切皆有可能,所以我说的每句话既是真又是假 :-)

回复 ,作者:libreman (未验证)

感谢 Seth 的文章。
我看到它主要是针对 CentOS,但我的系统上运行的是 Debian / Ubuntu。命令是相同的还是不同的?

再次感谢

不错的文章,Seth!

这真的比使用现成的本地 Git 服务器(如 Gitolite)更容易吗?
http://gitolite.com/gitolite/index.html

不一定更容易,但话说回来,有时人们需要/想要的是个人 git 服务器。其他时候,人们需要或想要的是 gitolite。这完全取决于用例和偏好。

此外,熟能生巧,而且没有比练习更能了解 git 复杂性的方法了。即使只是为了练习,设置 git 服务器也是一项非常有用的活动。

回复 ,作者:Dougie Lawson (未验证)

ssh 用户能否覆盖他们的 shell,以便他们不使用 git shell?

我不完全理解你的意思,但就像管理服务器时的许多其他事情一样,很多可能性取决于你如何配置。我不知道有什么方法可以“突破” git-shell 进入 Unix shell,如果用户没有在服务器上启动远程 shell 的能力(我发现很难启动 /usr/bin/false)。除非你指的是使用我还未听说过的漏洞……但在那种情况下,可能性是无限的,因为我们无法知道我们不知道的漏洞。

回复 ,作者:Lewis Cowles (未验证)

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.