如何编写一个全新的 Git 协议

曾几何时,我在持续追踪自己的文件方面遇到一些问题。通常,我忘了自己是否将文件保存在自己的桌面电脑、笔记本电脑或者电话上,或者保存在了云上的什么地方。更有甚者,对非常重要的信息,像密码和Bitcoin的密匙,仅以纯文本邮件的形式将它发送给自己让我芒刺在背。

我需要的是将自己的数据存放一个git仓库里,然后将这个git仓库保存在一个地方。我可以查看以前的版本而且不用提心数据被删除。更最要的是,我已经能熟练地在不同电脑上使用git来上传和下载文件。

但是,如我所言,我并不想简单地上传我的密匙和密码到GitHub或者BitBucket,哪怕是其中的私有仓库。

一个很酷的想法在我脑中升腾:写一个工具来加密我的仓库,然后再将它Push到Backup。遗憾的是,不能像平时那样使用 git push命令,需要使用像这样的命令:

至少,在我发现git-remote-helpers以前是这样想的。

Git remote helpers

我在网上找到一篇git remote helpers的文档。

原来,如果你运行命令

Git会首先检查是否内建了asdf协议,当发现没有内建时,它会检查git-remote-asdf是否在PATH(环境变量)里,如果在,它会运行 git-remote-asdf origin asdf://example.com/repo 来处理本次会话。

同样的,你可以运行

很遗憾的是,我发现文档在真正实现一个helper的细节上语焉不详,而这正是我需要的。但是随后,我在Git源码中找到了一个叫git-remote-testgit.sh的脚本,它实现了一个用来测试git远程辅助系统的testgit。 它基本实现了从同样文件系统的本地仓库推送和抓取功能。所以来让git调用git-remote-asdf origin 。

就一样了。

同样地,你可以透过testgit协议向本地仓库中推送或者从中抓取。

在本文件中,我们将浏览git-remote-testgit的源码并以Go语言实现一个全新的helper分支: git-remote-go。过程中,我将解释源码的意思,以及在实现我自己的remote helper(git-remote-grave)中领悟到的种种.

基础知识

为了后面的章节理解方面,让我们先学习一些术语和基本机制。

当我们运行

$ git push myremote master

Git会运行以下命令来实例化一个新的进程

注意:第一个参数是remote name,第二个参数是url.

当你运行

下一条命令会实例化helper

因为远程origin会自动在克隆的仓库中自动创建。

当Git以一个新的进程实例化helper时,它会为 stdin,stdout及stderr通信打开管道。命令被通过stdin送达helper,helper通过stdout响应。任何helper在stderr上的输出被重定向到git的stderr(它可能是一个终端)。

下图说明了这种关系:

我需要说明的最后一点是如何区分本地和远程仓库。通常(但不是每一次),本地仓库是我们运行git的地方,远程仓库是我们需要连接的。

所以在push中,我们从本地仓库发送更改(的地方)到远程仓库。在Fetch中,我们从远程仓库抓取更改(的地方)到本地仓库。在Clone中,我们将远程仓库克隆到本地。

当git运行helper时,git将环境变量GIT_DIR设置为本地仓库的Git目录(比如:local/.git)。

项目开搞

在这篇文章中,我假设已经安装好Go语言,并且使用了环境变量$GOPATH指向一个为go的目录。

让我们以创建目录go/src/git-remote-go开始。这样的话我们就可以通过运行go install来安装我们的插件(假设go/bin在PATH中)。

在意识里面有了这一点后,我们可以编写go/src/git-remote-go/main.go最初的几行代码。

我将Main()分割了开来,因为当我们需要返回错误时错误处理将会变得更容易。这里我们也可以使用defet,因为log.Fatal调用了os.Exit但不调用defer里面的函数。

现在,让我们看下git-remote-testgit文件的最顶部,看下接下来需要做什么。

他们称之为alias的变量就是我们所说的remoteName。url则是同样的意义。

下一个声明是:

这里在Git目录下创建了一个命名空间以标识testgit协议和我们正在使用的远程路径。通过这样,testgit下面origin分支下的文件就能与backup分支下面的文件区分开来。

再下面,我们看到这样的声明:

此处确保了本地目录已被创建,如果不存在则创建。

让我们为我们的Go程序添加本地目录的创建。

紧接着上面的脚本,我们有以下几行:

这里快速谈论一下refs。

在git中,refs存放在.git/refs:

在上面的树中,remotes/origin/master包括了远程origin中mater分支下最近大量的提交。而heads/master则关联你本地mater分支下最近大量的提交。一个ref就像一个指向一次提交的指针。

refspec则可以让我把远程的refs的本地的refs映射起来。在上面的代码中,prefix就是会被远程refs保留的目录。如果远程的名称是原始的,那么远程master分支将会由.git/refs/testgit/origin/master所指定。这样就很基本地为远程的分支创建了指定协议的命名空间。

接下来的这一行则是refspec。这一行

可以扩展成

这意味着远程分支的映射看起来就像把refs/heads/*(这里的*表示任意文本)对应到refs/testgit/$alias/*(这里的*将会被前面的*表示的文本替换)。例如,refs/heads/master将会映射到refs/testgit/origin/master。

基本上来讲,refspec允许testgit添加一个新的分支到自己的树中,例如这样:

下一行

把$refspec设置成$GIT_REMOTE_TESTGIT_REFSPEC,除非它不存在,否则它会成为$default_refspec。这样的话就能通过testgit测试其他的refspecs了。我们假设都已经成功设置了$default_refspec。

最后,再下一行,

按照我们的理解,看起来像是如果$GIT_REMOTE_TESTGIT_REFSPEC存在却为空时则把$prefix设置成refs。

我们需要自己的refspec,所以需要添加这一行

紧随上面的代码,我们看到了

export GIT_DIR

关于$GIT_DIR的另一个事实就是如果它有在环境变量中设置,那么底层的git将会使用环境变量中$GIT_DIR的目录作为它的.git目录,而不再是本地目录的.git。这个命令使得未来全部插件的git命令都能在远程制品库的上下文中执行。

我们把这点转换成

当然请记住,那个$dir和我们变量中的localdir依然指向我们正在fetch或push的子目录。

main块里面还有一小段代码

按我们的理解是,如果$GIT_REMOTE_TESTGIT_NO_MARKS未设置,if语句中的内容将会被执行。

这些标识文件可以纪录像git fast-export和git fast-import这些传递过程中ref和blob的有关信息。有一点是非常重要的,即这些标识在各式各样的插件中都是一样的,所以他们都是保存在localdir中。

这里,$gitmarks关联着我们本地制品库中git写入的标识,$testgitmarks则保存远程处理写入的标识。

下面这两行有点像touch的使用,如果标识文件不存在,则创建一个空的。

我们自己的程序中需要这些文件,所以让我们以编写一个Touch函数开始。

现在我们可以创建标识文件了。

偶尔为街头独特的风景驻足,

如何编写一个全新的 Git 协议

相关文章:

你感兴趣的文章:

标签云: