手搓 Git 服务器(一):搭建一个简单的基于 SSH 的 Git 服务器
Git 是目前最流行的源代码控制系统,它是由 Linus Torvalds 以及其他开发内核的人员为了在 Linux 内核开发中使用而创建的。如果深入了解 Git 的传输协议,那么就会感受到 Unix 哲学中的简单性。Git 的中文文档介绍了 《服务器上的 Git - 协议》(英文文档),参考该文档,可以快速构建一个简单的 Git 服务器,并加入自定义的能力。
Git 支持基于 SSH 和 HTTP/HTTPS 的协议。本文介绍一种使用 Go 语言实现基于 SSH 的 Git 服务器的 智能协议 的方法。
Git 智能协议
Git 智能协议是一种用于在 Git 客户端和服务器之间进行高效通信的机制,“它需要在服务端运行一个进程,而这也是 Git 的智能之处——它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。 总共有两组进程用于传输数据,它们分别负责上传和下载数据”。当 git 客户端程序与 Git 服务器交互时,会在服务器端运行对应的程序,进行数据交换。如下图所示:

上传数据
当 Git 客户端上传数据到 Git 服务器(比如 git push origin main)时,实际上会在客户端运行 send-pack 进程,它通过 SSH 连接 Git 服务器,然后调用服务器端的 git-receive-pack 程序,git-receive-pack“会立即为它所拥有的每一个引用发送一行响应”。然后, send-pack 和 git-receive-pack 进行交互,完成文件上传。具体可见文档 “上传数据”一节。
下载数据
当 Git 客户端从 Git 服务器下载数据时,实际上会在客户端运行 fetch-pack,它通过 SSH 连接 Git 服务器,然后调用服务器端的 git-upload-pack 的程序,协商后续传输的数据,最终返回客户端下载到所需的数据。具体可见文档 “下载数据” 一节。
实现思路
当服务器安装了 Git 之后,那么机器上已经有了 git-receive-pack 和 git-upload-pack。比如,在 macOS 上使用 Homebrew 后,可以看到 Git 程序所包含的可执行程序:
$ ls -1 /usr/local/Cellar/git/2.46.0/bingitgit-cvsservergit-receive-packgit-shellgit-upload-archivegit-upload-packscalar因此,在这种情况下,只需要在文件系统上创建裸仓库,并对外提供 SSH 服务,那么就可以实现 Git 服务器。
使用 SSH 服务器实现极简的 Git 服务器
Linux 服务器本身就提供了 SSH 服务,只需要安装 Git,确保 git-receive-pack 和 git-upload-pack 位于系统路径之中,那么一个极简的 Git 服务器已经就绪了。在本地电脑可通过如下步骤尝试:
- 确保有一个可以使用 SSH 登录服务器的用户,比如
foo。 - 在服务器的文件系统上,创建一个裸仓库。比如在
/tmp/repositories/bar创建仓库:
mkdir -p /tmp/repositories/barcd /tmp/repositories/bargit init --bare- 找到任意的一个本地仓库,假定其包含
main分支。确保本机的 SSH 服务器已经开启并允许访问,那么可以将它推送到/tmp/repositories/bar仓库中:
git remote add local localhost:/tmp/repositories/bar/git push local main- 如果想测试拉取仓库,那么在新的目录克隆仓库:
mkdir -p /tmp/repositories/cd /tmp/repositoriesgit clone localhost:/tmp/repositories/bar/ bar2如果一切顺利,那么 /tmp/repositories/bar 就是服务器端的仓库,/tmp/repositories/bar2 是克隆到客户端的仓库。
这就是一个极简的 Git 服务器。如果仅仅是用于代码的上传下载,这样的服务器已经够用了。但是,在实际中,如果仅仅是为了搭建 Git 服务器而开发 SSH 服务器的所有功能和权限,那不太满足“最小特权”原则;另外,真实的 Git 的服务器可能还需要一些其他的周边功能,比如用户管理、权限、日志等。那么,使用服务器上的原生 SSH 服务器就显得过于简陋了。
通过编程实现 Git 服务器
对于复杂的场景,需要通过编程语言实现 Git 服务器。具体地说,实现一个 SSH 服务,除了调用 git-receive-pack 和 git-upload-pack 程序与 Git 客户端程序交互之外,还应该能够添加复杂的逻辑。
技术选型
SSH 服务器
对于大部分编程语言,都有开源的 SSH 相关的库,有些比较底层,有些进行了高层的抽象。选择一个使用起来顺手的,并且足够安全的库,然后编写集成代码,即可手搓一个 Git 服务器。Go 语言有一个开源的 SSH 软件包 gliderlabs/ssh 。它封装了标准库里的 crypto/ssh ,提供了构建 SSH 服务器的高层接口。
命令行处理
对于客户端发送给SSH服务器的命令,需要进行解析后使用。判断其调用了 git-receive-pack、git-upload-pack,还是其它。尽管可以自行解析,但是这不一定是简单的任务。可使用第三方库google/shlex解析
进程间通行
当 Git 服务器接收到 git-receive-pack 和 git-upload-pack 命令时,需要调用相应的程序,然后与客户端交互。为此,可使用标准库里的 os/exec 调用命令行程序,将其标准输入与标准输出与客户端连接。
示例
代码示例
以下是一个简单的 Git 服务器的示例。它通过 SSH 进行认证,然后在接收到命令和客户端上传数据后,打印了一些信息:
package main
import ( "fmt" "log" "os" "os/exec"
"github.com/gliderlabs/ssh" "github.com/google/shlex")
var GIT_RECEIVE_PACK_CMD = "git-receive-pack"var GIT_UPLOAD_PACK_CMD = "git-upload-pack"
type SshOut struct { s ssh.Session}
type SshIn struct { session ssh.Session}
func (writer SshOut) Write(data []byte) (int, error) { return writer.s.Write(data)}
func (reader SshIn) Read(data []byte) (int, error) { return reader.session.Read(data)}
func runGitReceivePackCmd(session ssh.Session, repoPath string) error { cmd := exec.Command(GIT_RECEIVE_PACK_CMD, repoPath) sshOut := SshOut{s: session}
cmd.Stdout = sshOut cmd.Stderr = session.Stderr() cmd.Stdin = SshIn{session: session}
return cmd.Run()}
func runGitUploadPackCmd(session ssh.Session, repoPath string) error { cmd := exec.Command(GIT_UPLOAD_PACK_CMD, repoPath) sshOut := SshOut{s: session}
cmd.Stdout = sshOut cmd.Stderr = session.Stderr() cmd.Stdin = SshIn{session: session}
return cmd.Run()}
func main() { ssh.Handle(func(s ssh.Session) { fmt.Println("ssh raw command:", s.RawCommand())
args, err := shlex.Split(s.RawCommand()) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) return }
if args[0] == GIT_RECEIVE_PACK_CMD { if err := runGitReceivePackCmd(s, args[1]); err != nil { fmt.Fprintln(os.Stderr, err.Error()) } } else if args[0] == GIT_UPLOAD_PACK_CMD { if err := runGitUploadPackCmd(s, args[1]); err != nil { fmt.Fprintln(os.Stderr, err.Error()) } } })
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile(".ssh/id_ed25519")))}测试运行
将认证公钥放在 .ssh/id_ed25519 目录后运行程序:
go run main.go克隆项目
使用 git 命令克隆项目:
git clone ssh://localhost:2222/tmp/repositories/bar bar3此时 Git 服务器打印出了 git 客户端发送的原始命令:
ssh raw command: git-upload-pack '/tmp/repositories/bar'推送代码
在代码库中添加文件,然后推送到 Git 服务器:
git push origin main此时 Git 服务器打印的 SSH 原始命令和返回给客户端的数据如下:
ssh raw command: git-receive-pack '/tmp/repositories/bar'拉取代码
当使用 git remote update 更新远程分支列表,或者使用 git fetch 从远程仓库获取最新的变更时,Git 服务器处理的是下载数据的指令,打印出的 SSH 原始命令都是:
ssh raw command: git-upload-pack '/tmp/repositories/bar'加入复杂逻辑
既然可以通过编程的方式实现一个 SSH 服务器,那么就可以在代码中加入复杂逻辑了:
- 用户的权限认证,可以使用 gliderlabs/ssh 的相关处理器
- 对于代码操作的 Hook,可以参考 Server-Side Hooks
中的说明,包括
pre-receive、update、post-receive等
结尾
得益于 Git 的实现哲学,搭建或者开发基于 SSH 的 Git 服务器,本质上是构建 SSH 服务器,然后在服务器端调用相关的命令行程序。