我们正在努力将ZendFramework迁移到Git。我们试图解决的一个问题是强制提交来自CLA签名者。
向我们提出的一种可能性是利用GPG签名提交消息的可能性。不幸的是,我在网上几乎找不到关于如何做到这一点的信息,所以我开始尝试一些解决方案。
我选择的方法利用了githooks,特别是commit-msg
钩子客户端,以及pre-receive
钩子服务器端。
客户端提交消息挂钩
commit-msg
钩子接收一个参数,即包含提交消息的临时文件的路径。这允许您在完成提交之前检查它或修改它。与所有git挂钩一样,非零退出状态将中止提交。
我的commit-msg
挂钩如下所示:
#!/bin/sh echo -n "GPG Signing message... "; PASSPHRASE=$(git config --get hooks.gpg.passphrase) if [ "" = "$PASSPHRASE" ];then echo "no passphrase found! Set it with git config --add hooks.gpg.passphrase <passphrase>" exit 1 fi gpg --clearsign --yes --passphrase $PASSPHRASE -o $1.asc $1 mv $1.asc $1 echo "[DONE]"
此挂钩要求您首先将GPG密钥的密码添加到您的localgit配置中,这可以按如下方式完成:
$ git config --add hooks.gpg.passphrase "mySecret"
一旦这个钩子就位,所有的提交消息都会被明确签名,导致提交日志如下所示:
commit f921f0defb18f8a5218d5c3346693dbb4179920e Author: Matthew Weier O'Phinney <somebody@example.com> Date: Tue Mar 23 17:18:35 2010 -0400 -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 how now, brown cow -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (GNU/Linux) iEYEARECAAYFAkupMCsACgkQtUV5aSPtKdqERQCeN5taRATpB4/XJZiP9Vs5FVNY PcoAn0OZbIIcn7nC01yxp9tY7HbxVVFu =C/Ju -----END PGP SIGNATURE-----
服务器端预接收钩子
pre-receive
钩子就没那么直接了。此挂钩通过STDIN
接收输入。每行由三个项目组成,由一个空格分隔:
[previous commit's sha1] [new commit's sha1] [refspec]
通常,只有新的sha1对我们有用。在内部,git实际上在跟踪新的提交,即使它在技术上还没有被存储库接受。这允许我们使用诸如gitshow
之类的工具来获取有关提交的信息并根据该信息采取行动。
我需要做的是检查GPG签名消息的提交消息;如果没有找到,则直接拒绝提交,但如果存在,则根据我的密钥环对其进行验证,如果签名消息无效则中止。
我最初使用gitshow--pretty="format:%b"[sha1]
但是,我发现git做了一些……奇怪的事情……来提交消息。前50个左右的字符被认为是提交的“主题”——在主题中发现的任何换行符都被默默地删除。这意味着,出于我的目的,我得到了一条永远无法验证的截断消息(因为GPG签名头被剥离);即使在格式中包含主题也不起作用,因为其中的换行符丢失了。我发现获取完整提交消息的唯一方法是使用gitshow--pretty=raw[sha1]
。然而,这也为我提供了提交标头和差异—这意味着我必须解析响应。
接下来是我做的一个PHP实现,它就是这样做的:抓取完整的消息并将其重定向到一个临时文件,解析该文件以获取提交消息,然后对其执行操作。
#!/usr/bin/php <?php echo "Checking for GPG signature... "; $fh = fopen('php://stdin', 'r'); $tmpdir = sys_get_temp_dir(); while (!feof($fh)) { $line = fgets($fh); list($old, $new, $ref) = explode(' ', $line); // Create a tmp file with the commit log $logTmp = tempnam($tmpdir, 'LOG_'); $body = shell_exec('git show --pretty=raw ' . $new . ' > ' . $logTmp); $msgTmp = tempnam($tmpdir, 'MESSAGE_'); // Scan the commit log for a commit message $log = fopen($logTmp, 'r'); $msg = fopen($msgTmp, 'a'); $signatureDetected = false; while (!feof($log)) { $line = fgets($log); if (preg_match('/^(commit(ter)?|tree|parent|author)\s/', $line)) { // Skip the commit log headers continue; } if (preg_match('/^diff\s/', $line)) { // Stop scanning when we reach the diff break; } if (preg_match('/^\s+-+BEGIN [A-Z]+ SIGNED MESSAGE/', $line)) { // We have a signed message, so start appending it // to a separate tmp file $signatureDetected = true; $line = preg_replace('/^\s+/', '', $line); fwrite($msg, $line); continue; } if ($signatureDetected) { // If we have detected a signed message, continue appending lines to // it. Commit message lines are indented, so strip indentation. $line = preg_replace('/^\s+/', '', $line); if ('' === $line) { $line = "\n";" } fwrite($msg, $line); } } fclose($log); fclose($msg); if (!signatureDetected) { // No signed message detected; report and abort unlink($logTmp); unlink($msgTmp); echo "no GPG signature detected; commit aborted\n"; exit(1); } $verification = shell_exec('gpg --verify ' . $msgTmp . ' 2>&1'); if (!preg_match('/Good signature/s', $verification)) { // Failed to verify signed message; report and abort unlink($logTmp); unlink($msgTmp); echo "invalid GPG signature; commit aborted\n"; exit(1); } unlink($logTmp); unlink($msgTmp); } echo "verified!\n"; exit(0);
可能有更优雅的方法来实现这一点,包括其他语言的解决方案。但是,它工作得很好。
结论
Git钩子非常强大,深入研究它们让我有信心在我们准备好向公众开放时,我可以为ZFgit存储库创建一些不错的自动化。
也就是说,我不知道我们是否真的会使用这样的提交签名,因为它有一些缺点:
- 提交签名并不是真正的跨平台。这可能是可以补救的,但需要使用不同操作系统和使用不同工具(例如EGit、TortoiseGit等)的人为客户端开发和提供签名机制。
- 它为那些引入了复杂性开发补丁。如果开发人员在没有安装
commit-msg
挂钩的情况下开始,那么他们必须创建一个新分支并随后进行压缩提交,以确保最终补丁可以进入规范存储库。 - 上述两个原因有点违背了转向分布式VCS的初衷——即简化开发并使其更加民主。
不管我们是否决定使用这种技术,在研究这个问题时,我看到很多人发帖希望实施提交签名,但不确定如何实现。也许这篇文章将成为许多人的起点。