发布于 2026-01-06 0 阅读
0

Git - 如何验证提交信息?Git 提交钩子、Git 分支和引用、git push 会发生什么?总结

Git - 如何验证提交信息?

Git提交钩子

Git 分支和引用

git push 时会发生什么?

概括

上次我写了一篇面向初学者的 Git 入门教程。这次我想分享一个 Git 中稍微高级一点的问题的解决方案。我需要解决以下问题:所有提交信息都必须遵循一些特定规则(例如,行的最大长度等),任何不符合这些规则的提交都不能被推送。

我原以为这个问题应该很容易解决,而且肯定很多人已经找到了解决方法,因为很多项目都需要验证提交信息。但实际上并非如此。让我来解释一下原因!

Git提交钩子

在 Git 中,你可以指定所谓的提交钩子(commit hooks)。这些脚本会在特定操作发生时被调用。提交钩子分为客户端钩子和服务器钩子。之所以用引号,是因为我们知道在 Git 中并没有明确区分服务器和客户端仓库的角色,每个仓库都可以同时扮演服务器和客户端的角色。你可以在 Git 仓库的 `.git/hooks` 目录下找到所有这些提交钩子。

有一个名为 commit-msg 的提交钩子,这正是我们需要的。当你调用 git commit 命令时,这个脚本会被调用,它会接收提交信息作为输入参数,如果返回值不是 0,则会丢弃该提交。听起来很不错。唯一的问题是,这个脚本会在“客户端”仓库提交时运行。这意味着,如果客户端(克隆服务器仓库的用户)从仓库的 .git/hooks 文件夹中移除了这个提交钩子,他就可以随意提交任何内容。所以,这可以作为初步检查,但仍然无法安全地解决问题。此外,commit_hooks 不受版本控制,因此你需要找到另一种方法(可能需要一些额外的脚本)将它们复制到客户端仓库。或者,你可以在 hooks 文件夹和受版本控制的文件夹之间创建一个符号链接。

如果我们想百分之百确保服务器仓库中不会出现无效的提交信息,我们需要在服务器端进行检查。

要在服务器端进行检查,一种方法是使用提交钩子。每当有人向服务器推送数据时,都会调用此提交钩子;如果它返回非零值,则会拒绝此次推送。

唯一的问题是,这个提交钩子没有明确的输入来知道具体推送了哪些提交。它可以从标准输入读取信息,而标准输入只包含 Git 引用的更改,格式如下:旧值 新值 引用名称。

现在如何确定哪些是新的提交?

首先深入了解一下 Git,学习一下引用机制。

Git 分支和引用

在 Git 中,每次推送提交时,你总是会推送到一个分支。默认情况下,你位于 master 分支上,但你可以随时创建新分支,这些分支是从现有分支衍生出来的。如果你执行 `git push` 命令,它会将你的分支推送到上游分支(如果存在)。如果你是从服务器获取的分支,它的上游分支会自动设置。上游分支始终是服务器上的一个分支。如果上游分支不存在,或者你处于分离头模式(你不在任何分支上,你的头指针指向一个随机提交),Git 会要求你指定要推送到的分支(例如 `git push origin master`)。

我们现在先退一步。什么是 Git ref?Git ref 就像一个指向仓库中特定提交的命名指针。所有引用都位于 `.git/ref` 目录下。分支本质上就是特殊的引用。它们也只是指向某个提交的指针,但如果你向该提交提交了新的内容,分支会自动更新,指向该分支上的最新提交。但它仅仅是一个命名指针,仅此而已。

git push 时会发生什么?

提交本身了解的信息有限。它们只知道自身的内容和父提交。如果是合并提交,则该提交可能有多个父提交;否则,只有一个父提交。仓库中的第一个“根”提交没有任何父提交。

因此,如果您调用 `git push` 命令,您总是会推送一个或多个分支(使用 `git push --all` 时会推送多个分支)。您首先会告知服务器该分支指向哪个新提交。而这正是您传递给 `pre-receive` 提交钩子的输入值。`push` 提交钩子还会检查该分支是否已存在于服务器上,如果已存在,则会将该分支的先前内容告知 `pre-receive` 钩子。

然后服务器会检查是否已存在该提交(提交存储在 .git/objects 目录下)。如果不存在,则从客户端获取该提交并检查其父提交。如果父提交不在服务器上,则将其也移动到服务器。此过程持续进行,直到找到服务器上的第一个父提交为止。

如何在 pre-receive hook 中确定哪些提交是新的?

最大的成就在于,pre-receive 钩子只告诉我们哪些引用被修改了,除此之外不做任何其他事情。我们的目标是验证所有新推送的提交信息,仅此而已。

第一种也是最简单的情况是,有人向一个先前已存在的分支推送了提交。在这种情况下,我们会得到引用的旧值和新值,然后使用 `git log old_hash..new_hash` 命令可以看到它们之间的提交。

有一种特殊情况,这种方法会显示比实际需要更多的提交:在合并提交的情况下,它会显示合并分支的全部内容,但该分支可能已经至少部分推送过了。

我还需要提及引用(或分支)被删除的情况。在这种情况下,新的哈希值将是 40 个 0,但也意味着无需验证任何提交消息。

最后一种情况是推送了一个新分支。在这种情况下,引用的旧哈希值为 40 个零,而我们得到的是引用的新哈希值。这意味着我们只有该分支上最新提交的哈希值。那么,我们该如何判断呢?经过一番研究,我的想法是采用与推送操作相同的方式:检查最新提交,然后跳转到其父提交;如果是合并提交,则对所有父提交执行相同的操作;当到达之前已推送过的分支上的提交时,停止此操作。

这个想法听起来不错,但如何判断服务器上是否已经存在某个提交呢?肯定有很多解决方案,但我花了不少时间才找到一个有效的。

我的解决方案是使用 `git branch --contains` 命令,它会返回一个包含特定提交历史记录的分支列表。但请注意!由于 Git 只存储分支上最新提交的引用,因此所有先于该提交的提交都位于该分支上。所以,如果我从主分支的某个点创建了一个分支,那么主分支上所有早于我的分支的提交也都会包含在我的分支中。还有一点需要注意:客户端上的分支和服务器端上的分支并不相同,而这正是我们解决问题的关键所在。

根据我的经验,服务器上的所有提交至少都属于一个分支,因为无法推送分离的提交。pre-receive 提交钩子会在引用更改之前调用。这意味着所有之前未推送的提交都不属于任何当前存在的分支,但所有已存在的提交都至少属于一个分支。而这正是我们可以利用的。

概括

让我总结一下在服务器端提交钩子中检查 Git 提交信息的解决方案。

从分支的最新提交开始,逐个父提交检查,并检查 `git branch --contains` 命令是否返回空列表。如果是,则验证其提交信息并检查其父提交;如果不是,则说明该提交之前已被推送,我们无需再对该分支进行任何操作。特别注意合并提交,务必检查其每个父提交。

我希望这个解决方案是正确的,到目前为止它通过了所有测试用例,我也希望它能帮助你解决你的任务。

文章来源:https://dev.to/rlxdprogrammer/git-how-to-validate-commit-messages-55m5