Git 凭证辅助工具

事情源于我想将局域网内服务器上的代码上传到互联网上的远程仓库:repo-133 -> repo-E431 -> repo-remote

  • 代码编写以及编译运行只能在 133 服务器上,此机器无法连接互联网;
  • 133 服务器可以访问我自己的笔记本 E431,毕竟我要通过 xshell 访问 133 进行开发;
  • E431 笔记本可以访问互联网,针对私有代码我一般使用开源中国作为远程仓库。

实现这个打算很容易,问题出在精益求精的摸索过程中。我在 E431 上中转时使用的是 Bitvise SSH Server,搭建方法参考 在 Windows 上搭建 Git 服务器。(我在 E431 上建立的并不是裸仓库)搭建完毕,从 repo-133 向其推送时会失败:git “fatal: no matching remote head”,当时因为有别的工作,通过将 repo-E431 检出一个无用分支规避错误。

最近有时间,想着上述解决方案太粗糙了,用着也不方便,就打算再花时间“打磨”一下,没曾想竟然用了整整两天的时间。主要是花在了弄清楚 git credential 机制(凭证辅助工具系统)上。

包括上述提到推送失败,原方案存在两个问题:将 E431 的版本库建立成 非 bare 的,133 向 E431 提交时必须绕开工作区所在分支;133 提交后,还需要在 E431 上手工再一次提交。针对上述两个问题,分别改进(133 上的代码、配置不需要做任何更改):E431 使用 git init --bare 重新建立仓库,并使用 git hook 自动提交。

上面的解决思路是正确的。将 E431 仓库改为裸仓库之后,因为不存在工作区所以就不会再有冲突,但同样的因为不存在工作区,在裸仓库中许多涉及工作区的命令都是不能用的,比如 git pull

git hook

参考 配置 Git Hook 帖子,了解 8.3 自定义 Git - Git 钩子,发现要实现我的目的只需要在 DirMonitor.git/hooks/ 目录下创建 post-receive 文件即可:

1
2
3
4
5
#!/bin/sh
echo "" && echo "start transfer..."
git push --all
echo "done" && echo ""
exit 0

虽然涉及到的知识点较多,但如果只要求实现上述目的,对涉及到的知识点完全不需要做什么了解就可以完成。

胜利在望,在 133 更新、提交并推送,咔嚓,折了。至此,遇到了第一个难题。根据报错的信息在网上各种查阅,都不对症,最后绕回来还是以 stackoverflow 上的两个解答为契机,结合自己使用 Bitvise SSH Server,有了猜测。Bitvise SSH Server 更换虚拟账户,新添 windows 本地账户(133 使用此账户访问远程主机)解决了此问题。

我碰到的问题和第一篇问答中报错信息相仿 “git error: cannot spawn .git/hooks/post-receive: No such file or directory”,但赞同数最高的答案是 SHEBANG,很明显这不是我的问题,第一次看到这篇问答时忽略了其余的回答。等后来还是解决不了,偶然撞上第二篇问答,相互佐证,猜测我碰到的问题可能类似:

Apache must run as a regular user instead of Local System, in order to benefit from the environment variables defined for said regular user.

事实上,我新增 windows 系统登录用户到 Bitvise SSH Server,并将 133-repo 访问远程的用户同步修改后的确不再报此错误,能够执行 hook 脚本并打印输出,但在 git push -all 时卡死,始终不返回——欲知后事如何,且看下节精彩。

扩展学习:

在计算机科学中,Shebang(也称为 Hashbang )是一个由井号和叹号构成的字符序列 #! ,其出现在文本文件的第一行的前两个字符。引用自 维基百科

  1. post-receive 和 post-update 的区别
  2. unset GIT_DIR 存在的必要:源于 使用git钩子自动部署服务
  3. 脚本语言
  4. expect 语法:源于 使用 expect/git hooks 实现项目在服务器端的自动部署

交互式命令

面对上一节末尾出现的问题,毫无思路,在网上也查不到任何信息。只能自己摸索,而我根本不会写脚本,只能将 post-receive 中的 git push 换成其他的 git 命令,比如 git remote 和 git remote show origin。因为实在查阅不到什么有价值的信息,而且僵在这个问题上让我很烦,所以就自行摸索了(可能稍微懂脚本的同学一眼就能看到问题所在,╮(╯▽╰)╭)穷则思变

发现 git remote 本地操作的命令可以正常执行;但 git remote show origin 这种需要网络访问的就存在问题。进一步使用 ping git.oschina.net 过滤掉“网络可能未连通”之后,将矛头对准了交互。事后弄清原因之后,发现造成这种困扰并让我花费很多时间才做出正确的推断是一件很“凑巧”的事情:

如果使用 https:// 协议

  • 使用 git credential 存在多种凭证辅助工具:cache、store 和 manager
  • [√]不使用 git credential,直接在 url 中包含用户名和密码 https://username:password@github.com/username/repository.git
  • 不使用 git credential,使用常规 url。在每次访问远程时都手动输入用户名、密码。

如果使用 ssh:// 协议

  • 使用 passphrase
  • [√]不使用 passphrase

新版本 Git for Windows(2.9)一路默认安装,并使用了 Windows Credential Store 凭证辅助工具,以至于我在 E431 上敲入 git push、git remote show origin 等命令时直接就执行了,无需输入用户名、密码(我使用的 https:// 协议,并且在之前我肯定输入了 username & password,但我却无意识究竟发生了什么,代表着什么——这个下一节介绍)

为了证实猜测,我做了两次尝试验证。悲伤的是,两次验证的方向都是正确的,但源于别的小问题,却阻碍了我坚定信心,沿着既定方向一路摸索下去的决心,推迟了我看到真相的时间。

  1. 使用 https:// 协议,放弃 git credential 凭证辅助工具,直接在 url 中包含用户名和密码

    这个解决方案是正确的。可是 https://username:password@github.com/username/repository.git 这样子写有个例外,就是 password 不能包含 /。当时含有 / 报错 “Couldn’t resolve host ‘username’”,害的我一度以为 OSChina 和 Github 放弃了这种方法,毕竟使用明文密码很不友好。

    后来我将 OSChina 账户的密码改了,不再包含 /,就顺利地执行了。

  2. 使用 ssh:// 协议。虽然我中间尝试过改用此协议,但我忽略了 passphrase 的存在,当初 ssh-keygen 时敲入了密码,造成现在即便使用 ssh:// 协议,依旧存在交互。

    重新生成密钥,不再使用 passphrase,同样顺利地执行了。

遗留问题:

  1. 如果坚持使用含有 / 字符的密码,那么该如何在 url 使用密码呢? Clone a repo with Slash in Password 无解
  2. 如果 OSChina 或 Github 使用了双重认证,对此有何影响?
  3. 我在 Git for Windows 中执行帖子 SSH私钥取消密码(passphrase) 中描述的命令时直接僵死,没有反应,为什么?

git credential

这真的是一块我之前完全忽略了的知识点。只能说 Git 做的太好了,让普通用户感受不到凭证辅助工具系统的存在,对用户来说是透明的。

在这一节中,我就不再叙述学习过程中的曲折了,因为着实绕了个大弯才重新回到正道上。就直接说结果了。

git credential-xxx

凭证辅助工具有很多种,甚至用户可以自己写一个,只要满足特定的接口就可以。常见的几种凭证辅助工具:cache、store、windows 下的 Windows Credential Store、mac 下的 osxkeychain。

在 Git 中操作这几种工具的命令分别是:git credential-cachegit credential-storegit credential-manager(mac 下的我不知道)。这些命令的使用有点奇葩:

这个命令接收一个参数,并通过标准输入获取更多的参数。

前文提到凭证辅助工具必须满足特定的接口,此“特定的接口”就是指增加、查询和删除。当然,还可以有更多的功能,但至少得有这三个接口。接口的使用方式也是死的,分别通过 storegeterase 参数 —— 即引用中提到的“一个参数”中的“参数”。

git credential

git credential 命令的存在是为了抹掉辅助工具和 Git 之间的耦合,毕竟 Git 命令执行只关心交互时的凭证,不必操心这些凭证哪来的——鸡蛋好吃就行,何必找下蛋的那只鸡呢。

使用 git credential 之前可以通过 git config [--global] credential.helper xxx 配置凭证辅助工具。git credential 的用法和上述 git credential-xxx 的用法非常相似,只是参数不一样:

  • fill,可以理解为查询,从配置的所有辅助工具中查询,如果没查找,就伸手和用户要
  • approve,可以理解为添加,如果用户给的凭证经验证有效,(git)就调用这个命令将凭证保存到配置的所有辅助工具中
  • reject,可以理解为删除,调用这个命令会从配置的所有辅助工具中把与 description 匹配的凭证删掉

git credential 命令是给 Git 调用的接口,不是给程序员,不是给 git 使用者的——理解这一点很重要。

容易存在的一个误解是,上述加粗部分。git credential fill 从用户输入得到的用户名、密码是不会存储的。但 git push、git remote show origin 等远程交互命令会存储源于其对 git credential fill + core features + git credential approve 的封装:从标准输入得到用户名、密码之后,进行访问验证,如果成功 git 会调用 git credential approve 将此组 protocol、host、username、password 添加到辅助工具中。参考 _typical_use_of_git_credential

强调,git credential 凭证辅助工具系统是针对 http[s]:// 协议的,和 ssh:// 协议无关。

如果你使用的是 SSH 方式连接远端,并且设置了一个没有口令的密钥,这样就可以在不输入用户名和密码的情况下安全地传输数据。

然而,这对 HTTP 协议来说是不可能的 —— 每一个连接都是需要用户名和密码的。 这在使用双重认证的情况下会更麻烦,因为你需要输入一个随机生成并且毫无规律的 token 作为密码。幸运的是,Git 拥有一个凭证系统来处理这个事情。

扩展学习:

git 存在三个层次的配置文件?全局的应该是在 D:\Program Files\Git\mingw32\etc\gitconfig 下;用户主目录下应该有一份 .gitconfig;每个 repo 下有一份,在 .git/config 文件。修改时分别用什么参数呢?

windows 下的凭证辅助工具

  1. 在 windows 下 cache 辅助工具不能用,git credential-cache 不是有效命令。

    参考链接:git: ‘credential-cache’ is not a git command

  2. 在 windows 下 store 辅助工具也有问题,执行 git credential-store XXX 会报错。

    参考链接:fatal: unable to get credential storage lock: File exists,解决方案意义不大,如果我坚持使用 store 辅助工具呢?

  3. 我一度还碰到 “Fatal: Exception encountered” 错误。

    参考链接:Fatal: Exception encountered.

  4. 有上述两个前提,在 windows 下凭证辅助工具只能用 git credential-manager 了。

    直接在开始菜单搜索框中输入“Windows Credential Store”,可以使用窗口操作凭证。

最后的问题

为什么 hook 脚本中交互式命令不能直接使用凭证辅助工具中保存的用户名、密码呢?有什么方法可以做到呢?