Git - 如何验证提交消息?






4.14/5 (3投票s)
要确保你验证了所有 GIT 提交消息并不容易,让我向你展示原因
引言
上次,我写了一篇针对初学者的 GIT 入门介绍。这次,我想提供一个关于 GIT 中一个稍微高级问题的解决方案。我必须解决以下问题:所有提交消息都需要遵循一些特定的规则(最大行长等),并且在提交消息不符合这些规则时,不允许进行任何提交。
我曾以为这个问题应该有一个简单的解决方案,而且肯定有很多人已经解决了,因为提交消息验证对于许多项目来说是必需的。事实上,这并不容易。让我来描述一下为什么!
Git 提交钩子
在 GIT 中,你可以指定所谓的提交钩子。这些脚本在特定操作发生时被调用。有些提交钩子在“客户端”运行,有些则在“服务器”运行。我使用引号是因为我们知道在 GIT 中,服务器和客户端仓库没有明确的角色定义,每个仓库都可以作为服务器和客户端。你可以在 git 仓库的 .git/hooks 目录下找到所有这些提交钩子。
有一个名为 commit-msg
的提交钩子,这正是我们为此目的所需。当你调用 git commit
命令时,会调用此脚本,它将 commit
消息作为输入参数,如果它不返回 0
,则提交将被放弃。这听起来很棒。唯一的问题是此脚本在“客户端”仓库的提交时运行。这意味着如果客户端(即克隆了服务器仓库的人)删除了仓库的 .git/hooks 文件夹中的此提交钩子,他们就可以提交任何他们想要的内容。因此,这可以作为初步检查,但仍然无法安全地解决问题。此外,commit_hooks
不受版本控制,因此你需要找到另一种方法(可能通过一些额外的脚本)将其复制到客户端仓库。或者,你可以在 hooks 文件夹和一个受版本控制的文件夹之间创建一个符号链接。
如果我们想百分之百确定服务器仓库不会出现无效的提交消息,我们需要在服务器端进行检查。
要在服务器端进行检查,一种可能性是使用提交钩子。这个提交钩子会在有人向服务器推送任何内容时被调用,如果它返回非零值,则推送将被拒绝。
唯一的问题是这个提交钩子没有关于哪个确切提交被推送的清晰输入。它可以从标准输入读取其输入,并且只包含 git 引用(reference)的更改,格式如下:旧值 新值 引用名称。
现在如何找出哪些是新的提交?
首先,深入了解 git 并学习有关引用的知识。
Git 分支和引用
当你向 git 推送提交时,你总是推送到一个分支。默认情况下,你处于 master 分支,但你可以随时创建新的分支,它们会从已有的分支分叉出来。如果你执行 git push,它会将其分支推送到其上游分支(upstream branch),如果存在的话。如果你从服务器拉取了分支,其上游分支会自动设置。上游分支始终是服务器上的分支。如果上游分支不存在,或者你处于分离头模式(detached head mode,即你不在任何分支上,你的 HEAD 只是指向一个随机提交),git 会要求你指定要推送到哪个分支(例如 git push
origin master)。
现在让我们退一步。什么是 git ref?git ref 就像一个指向你仓库中特定提交的命名指针。你可以在 .git/ref 目录下找到所有引用。分支只不过是特殊的引用。它们也只是指向一个提交的指针,但如果你向该提交添加新内容,分支会自动更改为指向该分支上的最新提交。但它只是一个命名指针,仅此而已。
Git Push 时会发生什么?
提交本身了解的信息不多。它们只知道自己的内容和父提交。在合并提交(merge commit)的情况下,该提交有多个父提交,否则只有一个。仓库中的第一个“root
”提交没有任何父提交。
因此,当你调用 git push 时,你总是推送一个或多个(使用 git push --all
)分支。你首先会通知服务器该分支指向的新提交是什么。这就是你作为 pre-receive 提交钩子收到的输入值。Push 提交钩子还会检查分支是否已存在于服务器上,如果存在,它会告知 pre-receive 钩子其先前的内容是什么。
然后服务器会检查它是否已经拥有该提交(提交存储在 .git/objects 下)。如果没有,它会从客户端获取该提交,并检查其父提交。如果父提交不在服务器上,父提交也会被移动到服务器。它会一直继续,直到遇到第一个位于服务器上的父提交。
如何在 pre-receive 钩子中找出哪些提交是新的?
最大的成就是 pre-receive 钩子只告诉我们哪些引用被更改成了什么,仅此而已。我们的目标是验证所有新推送的提交消息,仅此而已。
第一种也是最简单的情况是,如果有人向之前已经存在的某个分支推送了提交。在这种情况下,我们会得到引用的旧值和新值,使用 `git log old_hash..new_hash`,我们可以看到它们之间的提交。
有一种特殊情况,这种方法可能会显示比必要更多的提交:对于合并提交,它会显示合并分支的全部内容,尽管该分支可能已经被部分或全部推送了。
我还必须提到引用(或分支)被删除的情况。在这种情况下,新哈希将是 40 个零,但这也意味着不需要验证任何提交消息。
需要涵盖的最后一个情况是推送了新分支。在这种情况下,引用的旧哈希是 40 个零,我们得到了引用的新哈希。这意味着我们只有分支上最新提交的哈希。需要知道什么?经过一些调查,我的想法是做和 push 相同的事情。检查最新提交,然后跳转到其父提交,在合并提交的情况下,对所有父提交执行相同操作,并在达到该分支上已提前推送的提交时停止此活动。
这个想法听起来不错,但如何找出提交是否已经在服务器上?当然,有多种解决方案,但找到一个有效的方案花费了我一些时间。
我的解决方案是 `git branch --contains`。这个命令返回一个列表,列出包含特定提交在其历史记录中的分支。但请注意!由于 git 只存储分支上最新提交的引用,因此该分支上包含该提交的所有祖先提交。所以,如果我在 master 分支的某个点分叉出来,那么 master 分支上在我分支之前的所有提交也是我分支的一部分。还有一点需要注意:客户端上的分支和服务器上的分支是不同的,而这正是我们任务的解决方案。
根据我的经验,服务器上的所有提交至少属于一个分支,因为不可能推送一个分离的提交。pre-receive 提交钩子在更改引用之前被调用。这意味着所有之前未推送的提交不属于任何当前存在的现有分支,但所有之前已存在的提交至少属于一个分支。而这正是我们可以利用的事实。
摘要
让我总结一下在服务器端提交钩子中检查 git 提交消息的解决方案。
从分支的最新提交开始,逐个父提交向上检查,并查看 `git branch --contains` 对该提交是否返回空列表。如果是,则验证其提交消息并检查其父提交;如果不是,则该提交已被提前推送,在该分支上我们无事可做。请特别注意合并提交,要检查每个父提交。
我希望这个解决方案是正确的,到目前为止,它已经通过了所有测试用例,我也希望它能帮助你解决你的任务。